kanban-system 1.0.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/.env.example +76 -0
- package/CLAUDE.md +108 -0
- package/README.md +272 -0
- package/agents/_TEMPLATE.md +42 -0
- package/agents/backend-agent.md +81 -0
- package/agents/deploy-gate-agent.md +73 -0
- package/agents/frontend-agent.md +73 -0
- package/agents/monitor-agent.md +65 -0
- package/agents/orchestrator.md +91 -0
- package/agents/reviewer-codex.md +51 -0
- package/bin/cli.js +171 -0
- package/config.example.js +99 -0
- package/docs/adapting-to-your-project.md +155 -0
- package/docs/example-apex.md +86 -0
- package/docs/the-pattern.md +92 -0
- package/hooks/launchd.plist.template +66 -0
- package/hooks/pre-push.sample +61 -0
- package/lib/config.cjs +138 -0
- package/lib/detect/_template.cjs +63 -0
- package/lib/detect/rules.json +28 -0
- package/lib/detect/sentry.cjs +86 -0
- package/lib/detect/vercel.cjs +62 -0
- package/lib/gate/index.cjs +182 -0
- package/lib/runner/adapters/both.cjs +33 -0
- package/lib/runner/adapters/claude.cjs +119 -0
- package/lib/runner/adapters/codex.cjs +43 -0
- package/lib/runner/adapters/reviewer.cjs +91 -0
- package/lib/runner/budget.cjs +75 -0
- package/lib/runner/index.cjs +93 -0
- package/lib/runner/result-merger.cjs +58 -0
- package/lib/runner/worktree-manager.cjs +64 -0
- package/lib/watch/scheduler.cjs +164 -0
- package/package.json +59 -0
- package/playbooks/_TEMPLATE.html +54 -0
- package/playbooks/build-fail.html +57 -0
- package/playbooks/deploy-rollback.html +53 -0
- package/playbooks/e2e-regression.html +58 -0
- package/playbooks/playbook.css +26 -0
- package/playbooks/sentry-spike.html +53 -0
- package/server/kanban.cjs +1152 -0
- package/skills/archive.md +18 -0
- package/skills/gate.md +22 -0
- package/skills/standup.md +24 -0
- package/skills/triage.md +24 -0
- package/ui/kanban.html +628 -0
- package/ui/styles/kanban.css +436 -0
- package/ui/styles/progress.css +315 -0
- package/ui/styles/tokens.css +291 -0
package/ui/kanban.html
ADDED
|
@@ -0,0 +1,628 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="ko">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>APEX Sentinel · Multi-Agent Harness</title>
|
|
7
|
+
<link href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/variable/pretendardvariable-dynamic-subset.min.css" rel="stylesheet">
|
|
8
|
+
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|
9
|
+
<style>
|
|
10
|
+
:root {
|
|
11
|
+
--c-bg:#fafafa; --c-surface:#fff; --c-surface-2:#f4f4f5; --c-surface-3:#e4e4e7;
|
|
12
|
+
--c-border:#e4e4e7; --c-border-2:#d4d4d8;
|
|
13
|
+
--c-text:#18181b; --c-text-2:#52525b; --c-text-3:#71717a; --c-text-4:#a1a1aa;
|
|
14
|
+
--c-accent:#059669; --c-accent-h:#047857; --c-accent-bg:#d1fae5;
|
|
15
|
+
--st-idle-bg:#e4e4e7; --st-idle-fg:#374151; --st-idle-dot:#a1a1aa;
|
|
16
|
+
--st-gen-bg:#dbeafe; --st-gen-fg:#1e40af; --st-gen-dot:#3b82f6;
|
|
17
|
+
--st-val-bg:#fef3c7; --st-val-fg:#92400e; --st-val-dot:#f59e0b;
|
|
18
|
+
--st-pass-bg:#d1fae5; --st-pass-fg:#065f46; --st-pass-dot:#10b981;
|
|
19
|
+
--st-flag-bg:#fee2e2; --st-flag-fg:#991b1b; --st-flag-dot:#ef4444;
|
|
20
|
+
--m-claude:#c2410c; --m-claude-bg:rgba(194,65,12,0.1);
|
|
21
|
+
--m-codex:#047857; --m-codex-bg:rgba(4,120,87,0.1);
|
|
22
|
+
--m-both:#6d28d9; --m-both-bg:rgba(109,40,217,0.1);
|
|
23
|
+
--sh-sm:0 1px 2px rgba(0,0,0,0.04);
|
|
24
|
+
--sh-md:0 2px 6px rgba(0,0,0,0.06);
|
|
25
|
+
--hdr-h:52px;
|
|
26
|
+
--ops-w:340px;
|
|
27
|
+
}
|
|
28
|
+
[data-theme="dark"] {
|
|
29
|
+
--c-bg:#0a0a0b; --c-surface:#18181b; --c-surface-2:#27272a; --c-surface-3:#3f3f46;
|
|
30
|
+
--c-border:#27272a; --c-border-2:#3f3f46;
|
|
31
|
+
--c-text:#fafafa; --c-text-2:#d4d4d8; --c-text-3:#a1a1aa; --c-text-4:#71717a;
|
|
32
|
+
--c-accent:#10b981; --c-accent-h:#34d399; --c-accent-bg:rgba(16,185,129,0.15);
|
|
33
|
+
--st-idle-bg:#27272a; --st-idle-fg:#d4d4d8;
|
|
34
|
+
--st-gen-bg:#1e3a5f; --st-gen-fg:#93c5fd;
|
|
35
|
+
--st-val-bg:#3f2f0a; --st-val-fg:#fbbf24;
|
|
36
|
+
--st-pass-bg:#0a3d2e; --st-pass-fg:#6ee7b7;
|
|
37
|
+
--st-flag-bg:#3f1212; --st-flag-fg:#fca5a5;
|
|
38
|
+
--m-claude:#fb923c; --m-claude-bg:rgba(251,146,60,0.12);
|
|
39
|
+
--m-codex:#34d399; --m-codex-bg:rgba(52,211,153,0.12);
|
|
40
|
+
--m-both:#a78bfa; --m-both-bg:rgba(167,139,250,0.15);
|
|
41
|
+
}
|
|
42
|
+
*{margin:0;padding:0;box-sizing:border-box}
|
|
43
|
+
html,body{height:100%;overflow:hidden;background:var(--c-bg);color:var(--c-text);font-family:'Pretendard Variable',Pretendard,-apple-system,sans-serif;font-size:13px;line-height:1.5;-webkit-font-smoothing:antialiased;letter-spacing:-0.005em}
|
|
44
|
+
body{display:flex;flex-direction:column}
|
|
45
|
+
.hdr{height:var(--hdr-h);padding:0 20px;border-bottom:1px solid var(--c-border);background:var(--c-surface);display:flex;align-items:center;justify-content:space-between;flex-shrink:0;z-index:50}
|
|
46
|
+
.hdr-l{display:flex;align-items:center;gap:12px}
|
|
47
|
+
.brand{font-size:14px;font-weight:700;letter-spacing:-0.02em;color:var(--c-text)}
|
|
48
|
+
.brand-dot{color:var(--c-accent)}
|
|
49
|
+
.brand-sub{font-size:11px;color:var(--c-text-3);padding-left:10px;border-left:1px solid var(--c-border-2)}
|
|
50
|
+
.hdr-r{display:flex;align-items:center;gap:10px}
|
|
51
|
+
.stats{display:flex;background:var(--c-surface);border:1px solid var(--c-border);border-radius:7px;overflow:hidden}
|
|
52
|
+
.stat{padding:4px 13px;border-right:1px solid var(--c-border);text-align:center}
|
|
53
|
+
.stat:last-child{border-right:0}
|
|
54
|
+
.stat .n{font-size:16px;font-weight:700;color:var(--c-text);font-family:'JetBrains Mono',monospace;line-height:1.2}
|
|
55
|
+
.stat .l{font-size:9px;color:var(--c-text-3);text-transform:uppercase;letter-spacing:0.06em}
|
|
56
|
+
.theme-tog{background:var(--c-surface);border:1px solid var(--c-border-2);color:var(--c-text-2);padding:5px 9px;border-radius:6px;font-size:13px;cursor:pointer;line-height:1}
|
|
57
|
+
.theme-tog:hover{border-color:var(--c-text-3)}
|
|
58
|
+
.conn{font-family:'JetBrains Mono',monospace;font-size:10px;padding:3px 8px;border-radius:9999px;display:inline-flex;align-items:center;gap:4px;font-weight:600}
|
|
59
|
+
.conn .cd{width:6px;height:6px;border-radius:50%}
|
|
60
|
+
.conn[data-s="connected"]{color:var(--st-pass-fg);background:var(--st-pass-bg)} .conn[data-s="connected"] .cd{background:var(--st-pass-dot)}
|
|
61
|
+
.conn[data-s="disconnected"]{color:var(--st-flag-fg);background:var(--st-flag-bg)} .conn[data-s="disconnected"] .cd{background:var(--st-flag-dot)}
|
|
62
|
+
.filter-bar{padding:10px 20px;background:var(--c-surface);border-bottom:1px solid var(--c-border);display:flex;align-items:center;gap:6px;flex-shrink:0;overflow-x:auto}
|
|
63
|
+
.f-pill{background:var(--c-surface);border:1.5px solid var(--c-border);color:var(--c-text-2);padding:6px 13px;border-radius:9999px;font-size:12px;cursor:pointer;transition:all 0.12s;font-family:inherit;font-weight:600;display:inline-flex;align-items:center;gap:6px;line-height:1;white-space:nowrap;flex-shrink:0}
|
|
64
|
+
.f-pill:hover{border-color:var(--c-text-4);color:var(--c-text);background:var(--c-surface-2)}
|
|
65
|
+
.f-pill.active{background:var(--c-accent);color:#fff;border-color:var(--c-accent)}
|
|
66
|
+
.f-pill.active .f-cnt{background:rgba(255,255,255,0.25);color:#fff}
|
|
67
|
+
.f-cnt{background:var(--c-surface-3);color:var(--c-text-3);padding:1px 7px;border-radius:9999px;font-family:'JetBrains Mono',monospace;font-size:10px;font-weight:700;min-width:20px;text-align:center}
|
|
68
|
+
.f-divider{width:1px;height:18px;background:var(--c-border-2);margin:0 2px;flex-shrink:0}
|
|
69
|
+
.f-tag{display:inline-flex;align-items:center;gap:5px;background:var(--c-accent-bg);color:var(--c-accent);padding:5px 10px;border-radius:9999px;font-size:11px;font-weight:700;font-family:'JetBrains Mono',monospace;white-space:nowrap}
|
|
70
|
+
.f-tag .x{cursor:pointer;font-weight:600;opacity:0.7}
|
|
71
|
+
.f-tag .x:hover{opacity:1}
|
|
72
|
+
.filter-bar .right{margin-left:auto;display:flex;gap:6px;align-items:center;flex-shrink:0}
|
|
73
|
+
.btn-icon{background:var(--c-surface);border:1px solid var(--c-border-2);color:var(--c-text-2);padding:6px 11px;border-radius:7px;font-size:11.5px;cursor:pointer;font-family:inherit;font-weight:600;white-space:nowrap}
|
|
74
|
+
.btn-icon:hover{border-color:var(--c-text-3);color:var(--c-text)}
|
|
75
|
+
.f-search{background:var(--c-bg);border:1.5px solid var(--c-border-2);border-radius:7px;padding:5px 10px;font-size:12px;color:var(--c-text);font-family:inherit;width:160px;flex-shrink:0;transition:width 0.2s}
|
|
76
|
+
.f-search:focus{outline:none;border-color:var(--c-accent);width:220px}
|
|
77
|
+
.f-search::placeholder{color:var(--c-text-4)}
|
|
78
|
+
.pri-bar{padding:6px 20px;background:var(--c-bg);border-bottom:1px solid var(--c-border);display:flex;align-items:center;gap:5px;flex-shrink:0;overflow-x:auto}
|
|
79
|
+
.pri-lbl{font-size:10px;font-weight:700;color:var(--c-text-3);text-transform:uppercase;letter-spacing:0.05em;margin-right:4px;white-space:nowrap}
|
|
80
|
+
.pri-pill{border:1.5px solid transparent;padding:3px 10px;border-radius:9999px;font-size:10.5px;cursor:pointer;font-family:'JetBrains Mono',monospace;font-weight:700;transition:all 0.12s;white-space:nowrap;background:none}
|
|
81
|
+
.pri-pill.p-all{background:var(--c-surface-2);color:var(--c-text-2);border-color:var(--c-border)}
|
|
82
|
+
.pri-pill.p-all.active{background:var(--c-surface-3);color:var(--c-text);border-color:var(--c-text-3)}
|
|
83
|
+
.pri-pill.p-critical{background:#fef2f2;color:#dc2626;border-color:#fecaca}
|
|
84
|
+
.pri-pill.p-critical.active{background:#dc2626;color:#fff;border-color:#dc2626}
|
|
85
|
+
.pri-pill.p-high{background:#fff7ed;color:#c2410c;border-color:#fdba74}
|
|
86
|
+
.pri-pill.p-high.active{background:#c2410c;color:#fff;border-color:#c2410c}
|
|
87
|
+
.pri-pill.p-medium{background:#eff6ff;color:#1d4ed8;border-color:#bfdbfe}
|
|
88
|
+
.pri-pill.p-medium.active{background:#1d4ed8;color:#fff;border-color:#1d4ed8}
|
|
89
|
+
.pri-pill.p-low{background:#f7fee7;color:#65a30d;border-color:#bbf7d0}
|
|
90
|
+
.pri-pill.p-low.active{background:#65a30d;color:#fff;border-color:#65a30d}
|
|
91
|
+
[data-theme="dark"] .pri-pill.p-critical{background:rgba(220,38,38,0.12);color:#fca5a5;border-color:rgba(220,38,38,0.3)}
|
|
92
|
+
[data-theme="dark"] .pri-pill.p-high{background:rgba(194,65,12,0.12);color:#fdba74;border-color:rgba(194,65,12,0.3)}
|
|
93
|
+
[data-theme="dark"] .pri-pill.p-medium{background:rgba(29,78,216,0.15);color:#93c5fd;border-color:rgba(29,78,216,0.35)}
|
|
94
|
+
[data-theme="dark"] .pri-pill.p-low{background:rgba(101,163,13,0.12);color:#86efac;border-color:rgba(101,163,13,0.3)}
|
|
95
|
+
[data-theme="dark"] .pri-pill.p-critical.active{background:#dc2626;color:#fff}
|
|
96
|
+
[data-theme="dark"] .pri-pill.p-high.active{background:#c2410c;color:#fff}
|
|
97
|
+
[data-theme="dark"] .pri-pill.p-medium.active{background:#1d4ed8;color:#fff}
|
|
98
|
+
[data-theme="dark"] .pri-pill.p-low.active{background:#65a30d;color:#fff}
|
|
99
|
+
.pri-bar-right{margin-left:auto;font-size:10.5px;color:var(--c-text-3);font-family:'JetBrains Mono',monospace;font-weight:600;white-space:nowrap}
|
|
100
|
+
.agents-panel{padding:12px 20px 6px;background:var(--c-bg);border-bottom:1px solid var(--c-border);flex-shrink:0}
|
|
101
|
+
.agents-panel-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:8px}
|
|
102
|
+
.agents-panel-head .ttl{font-size:11.5px;font-weight:700;color:var(--c-text);letter-spacing:0.02em;text-transform:uppercase}
|
|
103
|
+
.agents-panel-head .toggle{background:transparent;border:0;color:var(--c-text-3);font-size:11px;cursor:pointer;font-family:inherit;font-weight:600;padding:3px 6px}
|
|
104
|
+
.agents-panel-head .toggle:hover{color:var(--c-text)}
|
|
105
|
+
.agents-grid{display:grid;grid-template-columns:1fr 1fr;gap:12px}
|
|
106
|
+
.agents-col{background:var(--c-surface);border:1px solid var(--c-border);border-radius:9px;overflow:hidden}
|
|
107
|
+
.agents-col-head{padding:8px 12px;background:var(--c-surface-2);border-bottom:1px solid var(--c-border);display:flex;align-items:center;justify-content:space-between}
|
|
108
|
+
.agents-col-head .name{font-size:11px;font-weight:700;color:var(--c-text);letter-spacing:0.04em;text-transform:uppercase}
|
|
109
|
+
.agents-col-head .meta{font-size:9.5px;color:var(--c-text-3);font-family:'JetBrains Mono',monospace;font-weight:600}
|
|
110
|
+
.ag-row{display:grid;grid-template-columns:10px 150px 1fr auto auto auto;gap:10px;align-items:center;padding:8px 12px;border-bottom:1px solid var(--c-border);cursor:pointer;transition:background 0.1s}
|
|
111
|
+
.ag-row:last-child{border-bottom:0}
|
|
112
|
+
.ag-row:hover{background:var(--c-surface-2)}
|
|
113
|
+
.ag-row.active{background:var(--c-accent-bg)}
|
|
114
|
+
.ag-row.active .ag-name{color:var(--c-accent-h)}
|
|
115
|
+
.ag-dot{width:9px;height:9px;border-radius:50%;flex-shrink:0}
|
|
116
|
+
.ag-name{font-size:12px;font-weight:700;color:var(--c-text);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
117
|
+
.ag-role{font-size:10.5px;color:var(--c-text-3);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
118
|
+
.ag-runner{font-family:'JetBrains Mono',monospace;font-size:9.5px;padding:2px 7px;border-radius:5px;font-weight:700;white-space:nowrap}
|
|
119
|
+
.ag-runner.r-claude{color:var(--m-claude);background:var(--m-claude-bg)}
|
|
120
|
+
.ag-runner.r-codex{color:var(--m-codex);background:var(--m-codex-bg)}
|
|
121
|
+
.ag-runner.r-both{color:var(--m-both);background:var(--m-both-bg)}
|
|
122
|
+
.ag-runner.r-reviewer-codex{color:var(--m-claude);background:var(--m-claude-bg);border:1px solid var(--m-codex)}
|
|
123
|
+
.ag-runner.r-reviewer-claude{color:var(--m-codex);background:var(--m-codex-bg);border:1px solid var(--m-claude)}
|
|
124
|
+
.ag-load{display:flex;align-items:center;gap:4px;font-family:'JetBrains Mono',monospace;font-size:10px;color:var(--c-text-3);font-weight:700;min-width:40px;justify-content:flex-end}
|
|
125
|
+
.ag-load .lp{width:6px;height:6px;border-radius:50%;background:var(--c-text-4)}
|
|
126
|
+
.ag-load.busy .lp{background:var(--st-gen-dot);animation:pulse 1.5s infinite}
|
|
127
|
+
.ag-load.busy{color:var(--st-gen-dot)}
|
|
128
|
+
.ag-detail-btn{background:transparent;border:1px solid var(--c-border-2);color:var(--c-text-2);padding:3px 9px;border-radius:5px;font-size:10px;font-weight:700;cursor:pointer;font-family:inherit;line-height:1;transition:all 0.1s;white-space:nowrap}
|
|
129
|
+
.ag-detail-btn:hover{background:var(--c-text);color:var(--c-bg);border-color:var(--c-text)}
|
|
130
|
+
@keyframes pulse{0%,100%{opacity:1}50%{opacity:0.4}}
|
|
131
|
+
.agents-collapsed .agents-grid{display:none}
|
|
132
|
+
.agents-collapsed .agents-panel{padding-bottom:10px}
|
|
133
|
+
.board{flex:1;min-height:0;display:grid;grid-template-columns:repeat(4,minmax(260px,1fr));gap:12px;padding:12px 16px 14px;overflow-x:auto;overflow-y:hidden;align-items:stretch;background:var(--c-bg)}
|
|
134
|
+
.col{display:flex;flex-direction:column;min-height:0;overflow:hidden;background:var(--c-surface-2);border:1px solid var(--c-border);border-radius:12px}
|
|
135
|
+
.col-head{flex-shrink:0;display:flex;align-items:center;gap:8px;padding:10px 12px 9px;border-bottom:1.5px solid var(--c-border);background:var(--c-surface-2)}
|
|
136
|
+
.col-dot{width:9px;height:9px;border-radius:50%;flex-shrink:0}
|
|
137
|
+
.col-lbl{font-size:12.5px;font-weight:700;color:var(--c-text)}
|
|
138
|
+
.col-cnt{margin-left:auto;font-family:'JetBrains Mono',monospace;font-size:11px;color:var(--c-text);background:var(--c-surface);padding:1px 8px;border-radius:9999px;font-weight:700;border:1px solid var(--c-border)}
|
|
139
|
+
.col[data-s="pending"] .col-dot{background:var(--st-idle-dot)}
|
|
140
|
+
.col[data-s="in_progress"] .col-dot{background:var(--st-gen-dot);animation:pulse 2s infinite}
|
|
141
|
+
.col[data-s="in_review"] .col-dot{background:var(--st-flag-dot)}
|
|
142
|
+
.col[data-s="completed"] .col-dot{background:var(--st-pass-dot)}
|
|
143
|
+
.col-cards{flex:1;min-height:0;overflow-y:auto;padding:8px;display:flex;flex-direction:column;gap:6px}
|
|
144
|
+
.col-cards::-webkit-scrollbar{width:4px}
|
|
145
|
+
.col-cards::-webkit-scrollbar-track{background:transparent}
|
|
146
|
+
.col-cards::-webkit-scrollbar-thumb{background:var(--c-surface-3);border-radius:9999px}
|
|
147
|
+
.col-cards::-webkit-scrollbar-thumb:hover{background:var(--c-border-2)}
|
|
148
|
+
.col-empty{color:var(--c-text-4);font-size:11px;text-align:center;padding:24px 0;font-style:italic}
|
|
149
|
+
.tc{background:var(--c-surface);border:1px solid var(--c-border);border-left:3px solid var(--c-text-4);border-radius:7px;padding:8px 10px;cursor:pointer;transition:box-shadow 0.12s,transform 0.12s;box-shadow:var(--sh-sm)}
|
|
150
|
+
.tc:hover{box-shadow:var(--sh-md);transform:translateY(-1px)}
|
|
151
|
+
.tc.disagreed{border-left-color:var(--st-flag-dot) !important;background:linear-gradient(90deg,rgba(239,68,68,0.05),transparent 60%),var(--c-surface)}
|
|
152
|
+
.tc.subtask{padding:6px 9px;border-left-width:2px;opacity:0.95}
|
|
153
|
+
.tc-top{display:flex;align-items:center;gap:4px;margin-bottom:5px;flex-wrap:wrap}
|
|
154
|
+
.tc-id{font-family:'JetBrains Mono',monospace;font-size:10px;color:var(--c-text-3);font-weight:700;flex-shrink:0}
|
|
155
|
+
.tc-id .pref{color:var(--c-text-4);font-weight:500}
|
|
156
|
+
.tc-pri{font-family:'JetBrains Mono',monospace;font-size:9px;padding:1px 5px;border-radius:3px;font-weight:700;text-transform:uppercase;letter-spacing:0.03em;flex-shrink:0}
|
|
157
|
+
.tc-pri.pri-critical{background:#fef2f2;color:#dc2626}
|
|
158
|
+
.tc-pri.pri-high{background:#fff7ed;color:#c2410c}
|
|
159
|
+
.tc-pri.pri-medium{background:#eff6ff;color:#1d4ed8}
|
|
160
|
+
.tc-pri.pri-low{background:#f7fee7;color:#65a30d}
|
|
161
|
+
[data-theme="dark"] .tc-pri.pri-critical{background:rgba(220,38,38,0.18);color:#fca5a5}
|
|
162
|
+
[data-theme="dark"] .tc-pri.pri-high{background:rgba(194,65,12,0.18);color:#fdba74}
|
|
163
|
+
[data-theme="dark"] .tc-pri.pri-medium{background:rgba(29,78,216,0.18);color:#93c5fd}
|
|
164
|
+
[data-theme="dark"] .tc-pri.pri-low{background:rgba(101,163,13,0.18);color:#86efac}
|
|
165
|
+
.tc-tag{font-family:'JetBrains Mono',monospace;font-size:9px;padding:1px 5px;border-radius:3px;font-weight:700;flex-shrink:0}
|
|
166
|
+
.tc-tag.tag-incident{background:#fef2f2;color:#dc2626}
|
|
167
|
+
.tc-tag.tag-security{background:#f5f3ff;color:#7c3aed}
|
|
168
|
+
.tc-tag.tag-harness{background:#eff6ff;color:#1d4ed8}
|
|
169
|
+
.tc-tag.tag-content{background:#f0fdf4;color:#15803d}
|
|
170
|
+
.tc-tag.tag-default{background:var(--c-surface-2);color:var(--c-text-3)}
|
|
171
|
+
[data-theme="dark"] .tc-tag.tag-incident{background:rgba(220,38,38,0.15);color:#fca5a5}
|
|
172
|
+
[data-theme="dark"] .tc-tag.tag-security{background:rgba(124,58,237,0.15);color:#c4b5fd}
|
|
173
|
+
[data-theme="dark"] .tc-tag.tag-harness{background:rgba(29,78,216,0.15);color:#93c5fd}
|
|
174
|
+
[data-theme="dark"] .tc-tag.tag-content{background:rgba(21,128,61,0.15);color:#86efac}
|
|
175
|
+
.tc-title{font-size:12px;line-height:1.4;color:var(--c-text);margin-bottom:6px;font-weight:500;word-break:break-word}
|
|
176
|
+
.tc.subtask .tc-title{font-size:11.5px;margin-bottom:5px;color:var(--c-text-2)}
|
|
177
|
+
.tc-meta{display:flex;align-items:center;gap:5px;flex-wrap:wrap}
|
|
178
|
+
.tc-ag{display:inline-flex;align-items:center;gap:4px;font-size:10px;color:var(--c-text-2);font-weight:600}
|
|
179
|
+
.tc-ag .agd{width:6px;height:6px;border-radius:50%;flex-shrink:0}
|
|
180
|
+
.tc-rn{font-family:'JetBrains Mono',monospace;font-size:9.5px;display:inline-flex;align-items:center;gap:3px;padding:1px 6px;border-radius:4px;background:var(--c-surface-2);border:1px solid var(--c-border)}
|
|
181
|
+
.tc-rn .mc{font-weight:700}
|
|
182
|
+
.tc-rn .mc.claude{color:var(--m-claude)}
|
|
183
|
+
.tc-rn .mc.codex{color:var(--m-codex)}
|
|
184
|
+
.tc-rn .arr{color:var(--c-text-4);font-size:9px}
|
|
185
|
+
.tc-agree{font-family:'JetBrains Mono',monospace;font-size:9.5px;padding:1px 6px;border-radius:9999px;margin-left:auto;font-weight:700}
|
|
186
|
+
.tc-agree.agreed{color:var(--st-pass-fg);background:var(--st-pass-bg)}
|
|
187
|
+
.tc-agree.disagreed{color:var(--st-flag-fg);background:var(--st-flag-bg)}
|
|
188
|
+
.tc-agree.pending{color:var(--c-text-3);background:var(--c-surface-2)}
|
|
189
|
+
.tc-prog{margin-top:6px;height:3px;background:var(--c-surface-3);border-radius:9999px;overflow:hidden}
|
|
190
|
+
.tc-prog .fill{height:100%;background:var(--c-accent);transition:width 0.3s}
|
|
191
|
+
.tc-prog .fill.done{background:var(--st-pass-dot)}
|
|
192
|
+
.tc-sub{margin-top:3px;display:flex;justify-content:space-between;font-size:9.5px;color:var(--c-text-3);font-family:'JetBrains Mono',monospace;font-weight:600}
|
|
193
|
+
.tc-st{font-family:'JetBrains Mono',monospace;font-size:9.5px;padding:2px 8px;border-radius:9999px;font-weight:700;display:inline-flex;align-items:center;gap:4px;line-height:1.4}
|
|
194
|
+
.tc-st .sd{width:5px;height:5px;border-radius:50%}
|
|
195
|
+
.tc-st[data-s="pending"]{background:var(--st-idle-bg);color:var(--st-idle-fg)} .tc-st[data-s="pending"] .sd{background:var(--st-idle-dot)}
|
|
196
|
+
.tc-st[data-s="in_progress"]{background:var(--st-gen-bg);color:var(--st-gen-fg)} .tc-st[data-s="in_progress"] .sd{background:var(--st-gen-dot);animation:pulse 2s infinite}
|
|
197
|
+
.tc-st[data-s="in_review"]{background:var(--st-flag-bg);color:var(--st-flag-fg)} .tc-st[data-s="in_review"] .sd{background:var(--st-flag-dot)}
|
|
198
|
+
.tc-st[data-s="completed"]{background:var(--st-pass-bg);color:var(--st-pass-fg)} .tc-st[data-s="completed"] .sd{background:var(--st-pass-dot)}
|
|
199
|
+
.modal-overlay{position:fixed;inset:0;background:rgba(0,0,0,0.5);z-index:200;display:none;align-items:center;justify-content:center;backdrop-filter:blur(2px)}
|
|
200
|
+
.modal-overlay.open{display:flex}
|
|
201
|
+
.modal{background:var(--c-surface);border-radius:12px;width:90vw;max-width:760px;max-height:90vh;display:flex;flex-direction:column;box-shadow:0 20px 60px rgba(0,0,0,0.3);overflow:hidden;border:1px solid var(--c-border-2)}
|
|
202
|
+
.modal-head{padding:14px 18px;border-bottom:1px solid var(--c-border);display:flex;align-items:center;justify-content:space-between;background:var(--c-surface-2)}
|
|
203
|
+
.modal-head .m-name{display:flex;align-items:center;gap:8px}
|
|
204
|
+
.modal-head .m-dot{width:11px;height:11px;border-radius:50%}
|
|
205
|
+
.modal-head .m-title{font-size:14px;font-weight:700;color:var(--c-text);font-family:'JetBrains Mono',monospace}
|
|
206
|
+
.modal-head .m-group{font-size:9.5px;padding:2px 7px;border-radius:9999px;background:var(--c-surface-3);color:var(--c-text-2);font-weight:700;text-transform:uppercase;letter-spacing:0.04em}
|
|
207
|
+
.modal-head .m-close{background:transparent;border:0;color:var(--c-text-3);font-size:20px;cursor:pointer;padding:4px 8px;line-height:1;border-radius:6px}
|
|
208
|
+
.modal-head .m-close:hover{background:var(--c-surface-3);color:var(--c-text)}
|
|
209
|
+
.modal-body{padding:16px 18px;overflow-y:auto;display:flex;flex-direction:column;gap:12px}
|
|
210
|
+
.f-row{display:grid;grid-template-columns:110px 1fr;gap:12px;align-items:flex-start}
|
|
211
|
+
.f-label{font-size:11px;font-weight:700;color:var(--c-text-2);text-transform:uppercase;letter-spacing:0.04em;padding-top:7px}
|
|
212
|
+
.f-input,.f-textarea,.f-select{background:var(--c-bg);border:1px solid var(--c-border-2);border-radius:7px;padding:6px 9px;font-size:12.5px;color:var(--c-text);font-family:inherit;width:100%;line-height:1.5}
|
|
213
|
+
.f-input:focus,.f-textarea:focus,.f-select:focus{outline:none;border-color:var(--c-accent);box-shadow:0 0 0 2px rgba(5,150,105,0.12)}
|
|
214
|
+
.f-textarea{font-family:'JetBrains Mono',monospace;font-size:12px;resize:vertical;min-height:60px}
|
|
215
|
+
.f-textarea.body{min-height:280px;font-size:11.5px;line-height:1.55}
|
|
216
|
+
.f-color-row{display:flex;align-items:center;gap:8px}
|
|
217
|
+
.f-color-row input[type="color"]{width:40px;height:30px;border:1px solid var(--c-border-2);border-radius:5px;padding:2px;cursor:pointer;background:transparent}
|
|
218
|
+
.f-hint{font-size:10.5px;color:var(--c-text-3);margin-top:4px}
|
|
219
|
+
.modal-foot{padding:12px 18px;border-top:1px solid var(--c-border);background:var(--c-surface-2);display:flex;align-items:center;justify-content:space-between;gap:8px}
|
|
220
|
+
.modal-foot .left{font-size:10px;color:var(--c-text-3);font-family:'JetBrains Mono',monospace}
|
|
221
|
+
.modal-foot .right{display:flex;gap:6px}
|
|
222
|
+
.btn-primary{background:var(--c-accent);color:#fff;border:0;padding:7px 16px;border-radius:7px;font-size:12px;font-weight:700;cursor:pointer;font-family:inherit}
|
|
223
|
+
.btn-primary:hover{background:var(--c-accent-h)}
|
|
224
|
+
.btn-primary:disabled{opacity:0.5;cursor:not-allowed}
|
|
225
|
+
.btn-ghost{background:transparent;border:1px solid var(--c-border-2);color:var(--c-text-2);padding:7px 14px;border-radius:7px;font-size:12px;font-weight:600;cursor:pointer;font-family:inherit}
|
|
226
|
+
.btn-ghost:hover{border-color:var(--c-text-3);color:var(--c-text)}
|
|
227
|
+
@media(max-width:1100px){.board{grid-template-columns:repeat(2,minmax(260px,1fr))}}
|
|
228
|
+
@media(max-width:900px){.agents-grid{grid-template-columns:1fr}}
|
|
229
|
+
@media(max-width:640px){
|
|
230
|
+
:root{--hdr-h:46px}
|
|
231
|
+
.hdr{padding:0 12px}
|
|
232
|
+
.brand-sub{display:none}
|
|
233
|
+
.filter-bar{padding:8px 12px}
|
|
234
|
+
.f-search{width:110px}
|
|
235
|
+
.f-search:focus{width:150px}
|
|
236
|
+
.pri-bar{padding:5px 12px}
|
|
237
|
+
.agents-panel{display:none}
|
|
238
|
+
.board{grid-template-columns:repeat(4,calc(100vw - 24px));gap:8px;padding:8px;scroll-snap-type:x mandatory;-webkit-overflow-scrolling:touch}
|
|
239
|
+
.col{scroll-snap-align:start;scroll-snap-stop:always}
|
|
240
|
+
.ag-row{grid-template-columns:9px 1fr auto;gap:8px}
|
|
241
|
+
.ag-row .ag-role-cell,.ag-row .ag-runner-cell{display:none}
|
|
242
|
+
}
|
|
243
|
+
/* ── Ops Thread (Telegram mirror) ─────────────────────────────────────────── */
|
|
244
|
+
.work-area{flex:1;display:flex;min-height:0;overflow:hidden}
|
|
245
|
+
.work-area > .board{flex:1;min-width:0}
|
|
246
|
+
body.ops-collapsed .ops-thread{width:0;border-left:0}
|
|
247
|
+
body.ops-collapsed .ops-thread > *{display:none}
|
|
248
|
+
.ops-thread{width:var(--ops-w);flex-shrink:0;display:flex;flex-direction:column;background:var(--c-surface);border-left:1px solid var(--c-border);min-height:0;transition:width 0.18s ease}
|
|
249
|
+
.ops-head{height:38px;padding:0 14px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid var(--c-border);flex-shrink:0;background:var(--c-surface-2)}
|
|
250
|
+
.ops-head .ttl{font-size:12px;font-weight:700;color:var(--c-text);display:flex;align-items:center;gap:7px}
|
|
251
|
+
.ops-head .ttl .tg-dot{width:7px;height:7px;border-radius:50%;background:var(--c-text-4)}
|
|
252
|
+
.ops-head .ttl.connected .tg-dot{background:var(--st-pass-dot)}
|
|
253
|
+
.ops-head .sub{font-size:10px;color:var(--c-text-3);font-family:'JetBrains Mono',monospace;margin-left:6px;font-weight:500}
|
|
254
|
+
.ops-head .ops-tog{background:transparent;border:1px solid var(--c-border-2);color:var(--c-text-2);padding:3px 8px;border-radius:6px;font-size:11px;cursor:pointer;line-height:1}
|
|
255
|
+
.ops-head .ops-tog:hover{border-color:var(--c-text-3);color:var(--c-text)}
|
|
256
|
+
.ops-msgs{flex:1;overflow-y:auto;padding:10px 12px;display:flex;flex-direction:column;gap:8px;min-height:0}
|
|
257
|
+
.ops-msgs::-webkit-scrollbar{width:8px}
|
|
258
|
+
.ops-msgs::-webkit-scrollbar-thumb{background:var(--c-border-2);border-radius:4px}
|
|
259
|
+
.ops-msgs::-webkit-scrollbar-thumb:hover{background:var(--c-text-4)}
|
|
260
|
+
.ops-msg{max-width:88%;padding:7px 11px;border-radius:11px;font-size:12px;line-height:1.5;word-wrap:break-word;white-space:pre-wrap}
|
|
261
|
+
.ops-msg .meta{font-size:9.5px;color:var(--c-text-3);font-family:'JetBrains Mono',monospace;margin-top:3px;letter-spacing:0.02em}
|
|
262
|
+
.ops-msg.you{align-self:flex-end;background:var(--c-accent);color:#fff;border-bottom-right-radius:3px}
|
|
263
|
+
.ops-msg.you .meta{color:rgba(255,255,255,0.75)}
|
|
264
|
+
.ops-msg.operator{align-self:flex-end;background:#0ea5e9;color:#fff;border-bottom-right-radius:3px}
|
|
265
|
+
.ops-msg.operator .meta{color:rgba(255,255,255,0.75)}
|
|
266
|
+
.ops-msg.claude,.ops-msg.assistant,.ops-msg.agent{align-self:flex-start;background:var(--c-surface-2);color:var(--c-text);border:1px solid var(--c-border);border-bottom-left-radius:3px}
|
|
267
|
+
.ops-msg.system{align-self:center;background:transparent;color:var(--c-text-3);font-size:10.5px;font-family:'JetBrains Mono',monospace;font-weight:600;border:0;padding:2px 6px;max-width:100%}
|
|
268
|
+
.ops-msg .tag{color:var(--c-accent);font-weight:700;cursor:pointer;text-decoration:underline;text-decoration-color:transparent;transition:text-decoration-color 0.12s}
|
|
269
|
+
.ops-msg .tag:hover{text-decoration-color:var(--c-accent)}
|
|
270
|
+
.ops-msg.you .tag,.ops-msg.operator .tag{color:#fff}
|
|
271
|
+
.ops-empty{align-self:center;color:var(--c-text-4);font-size:11px;padding:40px 16px;text-align:center;line-height:1.6}
|
|
272
|
+
.ops-empty kbd{display:inline-block;padding:1px 5px;border:1px solid var(--c-border-2);border-radius:4px;font-family:'JetBrains Mono',monospace;font-size:10px;background:var(--c-surface-2);color:var(--c-text-2)}
|
|
273
|
+
.ops-form{padding:8px 10px;border-top:1px solid var(--c-border);display:flex;gap:6px;flex-shrink:0;background:var(--c-surface)}
|
|
274
|
+
.ops-form textarea{flex:1;resize:none;border:1.5px solid var(--c-border-2);border-radius:7px;padding:6px 10px;font-family:inherit;font-size:12px;color:var(--c-text);background:var(--c-bg);min-height:32px;max-height:120px;line-height:1.4}
|
|
275
|
+
.ops-form textarea:focus{outline:none;border-color:var(--c-accent)}
|
|
276
|
+
.ops-form button{background:var(--c-accent);color:#fff;border:0;padding:0 14px;border-radius:7px;font-size:12px;font-weight:700;cursor:pointer;font-family:inherit;flex-shrink:0}
|
|
277
|
+
.ops-form button:hover{background:var(--c-accent-h)}
|
|
278
|
+
.ops-form button:disabled{opacity:0.5;cursor:not-allowed}
|
|
279
|
+
.ops-reopen{position:fixed;right:14px;bottom:14px;background:var(--c-accent);color:#fff;border:0;padding:9px 14px;border-radius:9999px;font-size:12px;font-weight:700;cursor:pointer;font-family:inherit;box-shadow:0 4px 12px rgba(0,0,0,0.18);z-index:100;display:none}
|
|
280
|
+
body.ops-collapsed .ops-reopen{display:inline-flex;align-items:center;gap:6px}
|
|
281
|
+
@media (max-width:1100px){.ops-thread{position:fixed;right:0;top:var(--hdr-h);bottom:0;z-index:80;box-shadow:-4px 0 16px rgba(0,0,0,0.12)}}
|
|
282
|
+
</style>
|
|
283
|
+
</head>
|
|
284
|
+
<body data-theme="light">
|
|
285
|
+
<header class="hdr">
|
|
286
|
+
<div class="hdr-l">
|
|
287
|
+
<div class="brand">APEX<span class="brand-dot">·</span>Sentinel</div>
|
|
288
|
+
<div class="brand-sub">multi-agent harness</div>
|
|
289
|
+
</div>
|
|
290
|
+
<div class="hdr-r">
|
|
291
|
+
<div class="stats">
|
|
292
|
+
<div class="stat"><div class="n" id="s-total">0</div><div class="l">전체</div></div>
|
|
293
|
+
<div class="stat"><div class="n" id="s-active">0</div><div class="l">진행중</div></div>
|
|
294
|
+
<div class="stat"><div class="n" id="s-review">0</div><div class="l">검토</div></div>
|
|
295
|
+
<div class="stat"><div class="n" id="s-done">0</div><div class="l">완료</div></div>
|
|
296
|
+
</div>
|
|
297
|
+
<div class="conn" id="conn" data-s="disconnected"><span class="cd"></span><span id="conn-l">연결중</span></div>
|
|
298
|
+
<button class="theme-tog" id="ops-tog-hdr" onclick="toggleOps()" title="Ops Thread 토글">💬</button>
|
|
299
|
+
<button class="theme-tog" id="theme-tog" onclick="toggleTheme()" title="테마 전환">☾</button>
|
|
300
|
+
</div>
|
|
301
|
+
</header>
|
|
302
|
+
<div class="filter-bar">
|
|
303
|
+
<button class="f-pill active" data-flt="all" onclick="setFilter('all')">전체 <span class="f-cnt" id="fc-all">0</span></button>
|
|
304
|
+
<button class="f-pill" data-flt="opsguard" onclick="setFilter('opsguard')">OpsGuard <span class="f-cnt" id="fc-ops">0</span></button>
|
|
305
|
+
<button class="f-pill" data-flt="examguard" onclick="setFilter('examguard')">ExamGuard <span class="f-cnt" id="fc-exam">0</span></button>
|
|
306
|
+
<div class="f-divider"></div>
|
|
307
|
+
<button class="f-pill" data-flt="cross-validated" onclick="setFilter('cross-validated')">CV <span class="f-cnt" id="fc-cv">0</span></button>
|
|
308
|
+
<button class="f-pill" data-flt="parents-only" onclick="setFilter('parents-only')">Phase만 <span class="f-cnt" id="fc-par">0</span></button>
|
|
309
|
+
<button class="f-pill" data-flt="incident" onclick="setFilter('incident')">🚨 Incident <span class="f-cnt" id="fc-incident">0</span></button>
|
|
310
|
+
<div id="agent-tag-wrap"></div>
|
|
311
|
+
<div class="right">
|
|
312
|
+
<input class="f-search" id="f-search" type="text" placeholder="검색 #ID·제목·agent" oninput="setSearch(this.value)" autocomplete="off">
|
|
313
|
+
<button class="btn-icon" onclick="toggleAgents()" id="ag-toggle-btn">에이전트</button>
|
|
314
|
+
<button class="btn-icon" onclick="loadAll()">↻</button>
|
|
315
|
+
</div>
|
|
316
|
+
</div>
|
|
317
|
+
<div class="pri-bar">
|
|
318
|
+
<span class="pri-lbl">Priority</span>
|
|
319
|
+
<button class="pri-pill p-all active" data-pri="all" onclick="setPriority('all')">전체</button>
|
|
320
|
+
<button class="pri-pill p-critical" data-pri="critical" onclick="setPriority('critical')">critical</button>
|
|
321
|
+
<button class="pri-pill p-high" data-pri="high" onclick="setPriority('high')">high</button>
|
|
322
|
+
<button class="pri-pill p-medium" data-pri="medium" onclick="setPriority('medium')">medium</button>
|
|
323
|
+
<button class="pri-pill p-low" data-pri="low" onclick="setPriority('low')">low</button>
|
|
324
|
+
<span class="pri-bar-right" id="visible-cnt"></span>
|
|
325
|
+
</div>
|
|
326
|
+
<section class="agents-panel" id="agents-panel">
|
|
327
|
+
<div class="agents-panel-head">
|
|
328
|
+
<div class="ttl">에이전트 14개 · 클릭 시 해당 task만 필터</div>
|
|
329
|
+
<button class="toggle" onclick="toggleAgents()">접기 ▲</button>
|
|
330
|
+
</div>
|
|
331
|
+
<div class="agents-grid">
|
|
332
|
+
<div class="agents-col">
|
|
333
|
+
<div class="agents-col-head"><span class="name">OpsGuard</span><span class="meta" id="ag-meta-ops">· 0 active</span></div>
|
|
334
|
+
<div id="ag-rows-ops"></div>
|
|
335
|
+
</div>
|
|
336
|
+
<div class="agents-col">
|
|
337
|
+
<div class="agents-col-head"><span class="name">ExamGuard</span><span class="meta" id="ag-meta-exam">· 0 active</span></div>
|
|
338
|
+
<div id="ag-rows-exam"></div>
|
|
339
|
+
</div>
|
|
340
|
+
</div>
|
|
341
|
+
</section>
|
|
342
|
+
<div class="modal-overlay" id="task-modal" onclick="if(event.target===this)closeTaskModal()">
|
|
343
|
+
<div class="modal" role="dialog" aria-modal="true" style="max-width:820px">
|
|
344
|
+
<div class="modal-head">
|
|
345
|
+
<div class="m-name">
|
|
346
|
+
<span class="m-dot" id="t-agent-dot"></span>
|
|
347
|
+
<span class="m-title" id="t-id">#</span>
|
|
348
|
+
<span class="tc-st" id="t-status-pill" data-s="pending"><span class="sd"></span>대기</span>
|
|
349
|
+
<span class="m-group" id="t-agent-tag">—</span>
|
|
350
|
+
</div>
|
|
351
|
+
<button class="m-close" onclick="closeTaskModal()" title="닫기 (Esc)">×</button>
|
|
352
|
+
</div>
|
|
353
|
+
<div class="modal-body">
|
|
354
|
+
<div style="font-size:16px;font-weight:700;line-height:1.4;margin-bottom:4px;color:var(--c-text)" id="t-title-h">—</div>
|
|
355
|
+
<div id="t-cv-bar" style="display:none;padding:8px 12px;border-radius:8px;font-size:11.5px;font-family:'JetBrains Mono',monospace;background:var(--c-surface-2);border:1px solid var(--c-border)"></div>
|
|
356
|
+
<div class="f-row"><label class="f-label">상태</label><select class="f-select" id="t-status"><option value="pending">대기 (pending)</option><option value="in_progress">진행중 (in_progress)</option><option value="in_review">사람검토 (in_review)</option><option value="completed">완료 (completed)</option></select></div>
|
|
357
|
+
<div class="f-row"><label class="f-label">담당</label><select class="f-select" id="t-agent"></select></div>
|
|
358
|
+
<div class="f-row"><label class="f-label">Runner</label><select class="f-select" id="t-runner"><option value="">— 미지정 —</option><option value="claude">claude</option><option value="codex">codex</option><option value="both">both (cross-validated)</option><option value="reviewer:codex">reviewer:codex</option><option value="reviewer:claude">reviewer:claude</option></select></div>
|
|
359
|
+
<div class="f-row"><label class="f-label">설명</label><textarea class="f-textarea" id="t-desc" rows="6" placeholder="작업 계획 / 완료 기준 / 참고"></textarea></div>
|
|
360
|
+
<div class="f-row"><label class="f-label">결과 요약</label><textarea class="f-textarea" id="t-report" rows="3" placeholder="완료 시 한 줄 요약 + 후속 작업 제안"></textarea></div>
|
|
361
|
+
<div id="t-related" style="display:flex;flex-direction:column;gap:10px"></div>
|
|
362
|
+
<div id="t-timeline" style="display:none;font-size:11px;color:var(--c-text-3);font-family:'JetBrains Mono',monospace;padding:10px 12px;border:1px solid var(--c-border);border-radius:7px;background:var(--c-surface-2);line-height:1.7"></div>
|
|
363
|
+
</div>
|
|
364
|
+
<div class="modal-foot">
|
|
365
|
+
<span class="left" id="t-meta-info"></span>
|
|
366
|
+
<div class="right">
|
|
367
|
+
<button class="btn-ghost" onclick="deleteTaskFromModal()" id="t-delete" style="color:#dc2626;border-color:#fca5a5">삭제</button>
|
|
368
|
+
<button class="btn-ghost" onclick="closeTaskModal()">취소</button>
|
|
369
|
+
<button class="btn-primary" id="t-save" onclick="saveTaskModal()">저장</button>
|
|
370
|
+
</div>
|
|
371
|
+
</div>
|
|
372
|
+
</div>
|
|
373
|
+
</div>
|
|
374
|
+
<div class="modal-overlay" id="agent-modal" onclick="if(event.target===this)closeAgentModal()">
|
|
375
|
+
<div class="modal" role="dialog" aria-modal="true">
|
|
376
|
+
<div class="modal-head">
|
|
377
|
+
<div class="m-name"><span class="m-dot" id="m-dot"></span><span class="m-title" id="m-title">agent</span><span class="m-group" id="m-group">—</span></div>
|
|
378
|
+
<button class="m-close" onclick="closeAgentModal()" title="닫기 (Esc)">×</button>
|
|
379
|
+
</div>
|
|
380
|
+
<div class="modal-body">
|
|
381
|
+
<div class="f-row"><label class="f-label">Mission</label><textarea class="f-textarea" id="f-mission" rows="2" placeholder="이 에이전트가 막아야 할 실패 모드 (한 문장)"></textarea></div>
|
|
382
|
+
<div class="f-row"><label class="f-label">Role</label><div><input class="f-input" id="f-role" placeholder="짧은 역할 설명"><div class="f-hint">에이전트 패널 row의 두 번째 컬럼에 표시되는 짧은 라벨입니다.</div></div></div>
|
|
383
|
+
<div class="f-row"><label class="f-label">Runner</label><select class="f-select" id="f-runner"><option value="claude">claude</option><option value="codex">codex</option><option value="both">both (cross-validated)</option><option value="reviewer:codex">reviewer:codex</option><option value="reviewer:claude">reviewer:claude</option></select></div>
|
|
384
|
+
<div class="f-row"><label class="f-label">Group</label><select class="f-select" id="f-group"><option value="opsguard">opsguard</option><option value="examguard">examguard</option></select></div>
|
|
385
|
+
<div class="f-row"><label class="f-label">Color</label><div class="f-color-row"><input type="color" id="f-color" value="#71717a"><input class="f-input" id="f-color-hex" style="max-width:120px;font-family:'JetBrains Mono',monospace" placeholder="#hex"></div></div>
|
|
386
|
+
<div class="f-row"><label class="f-label">Body</label><div><textarea class="f-textarea body" id="f-body" placeholder="Triggers / Inputs / Outputs / Cross-validation policy / Failure handling / Examples"></textarea><div class="f-hint">Markdown 본문. 저장 시 자동으로 ## Changelog 섹션에 변경 이력이 append 됩니다.</div></div></div>
|
|
387
|
+
<div class="f-row"><label class="f-label">변경 이유</label><input class="f-input" id="f-changenote" placeholder="예: 'proctor 9종 이벤트에 screen-record 추가'"></div>
|
|
388
|
+
</div>
|
|
389
|
+
<div class="modal-foot">
|
|
390
|
+
<span class="left" id="m-path"></span>
|
|
391
|
+
<div class="right"><button class="btn-ghost" onclick="closeAgentModal()">취소</button><button class="btn-primary" id="m-save" onclick="saveAgentModal()">저장</button></div>
|
|
392
|
+
</div>
|
|
393
|
+
</div>
|
|
394
|
+
</div>
|
|
395
|
+
<div class="work-area">
|
|
396
|
+
<main class="board">
|
|
397
|
+
<section class="col" data-s="pending">
|
|
398
|
+
<div class="col-head"><span class="col-dot"></span><span class="col-lbl">대기</span><span class="col-cnt" id="cn-pending">0</span></div>
|
|
399
|
+
<div class="col-cards" id="col-pending"></div>
|
|
400
|
+
</section>
|
|
401
|
+
<section class="col" data-s="in_progress">
|
|
402
|
+
<div class="col-head"><span class="col-dot"></span><span class="col-lbl">진행중</span><span class="col-cnt" id="cn-in_progress">0</span></div>
|
|
403
|
+
<div class="col-cards" id="col-in_progress"></div>
|
|
404
|
+
</section>
|
|
405
|
+
<section class="col" data-s="in_review">
|
|
406
|
+
<div class="col-head"><span class="col-dot"></span><span class="col-lbl">사람검토</span><span class="col-cnt" id="cn-in_review">0</span></div>
|
|
407
|
+
<div class="col-cards" id="col-in_review"></div>
|
|
408
|
+
</section>
|
|
409
|
+
<section class="col" data-s="completed">
|
|
410
|
+
<div class="col-head"><span class="col-dot"></span><span class="col-lbl">완료</span><span class="col-cnt" id="cn-completed">0</span></div>
|
|
411
|
+
<div class="col-cards" id="col-completed"></div>
|
|
412
|
+
</section>
|
|
413
|
+
</main>
|
|
414
|
+
<aside class="ops-thread" id="ops-thread">
|
|
415
|
+
<div class="ops-head">
|
|
416
|
+
<div class="ttl" id="ops-ttl"><span class="tg-dot"></span>Ops Thread<span class="sub" id="ops-tg-state">local</span></div>
|
|
417
|
+
<button class="ops-tog" onclick="toggleOps()" title="패널 접기">▶</button>
|
|
418
|
+
</div>
|
|
419
|
+
<div class="ops-msgs" id="ops-msgs">
|
|
420
|
+
<div class="ops-empty">메시지 없음.<br><br>입력창에 메시지를 보내면<br>여기와 텔레그램 양쪽에 기록됩니다.<br><br><kbd>Enter</kbd> 전송 · <kbd>Shift+Enter</kbd> 줄바꿈</div>
|
|
421
|
+
</div>
|
|
422
|
+
<form class="ops-form" id="ops-form" onsubmit="return sendOps(event)">
|
|
423
|
+
<textarea id="ops-input" rows="1" placeholder="메시지를 입력하세요…" oninput="autoGrowOps(this)" onkeydown="opsKey(event)" autocomplete="off"></textarea>
|
|
424
|
+
<button type="submit" id="ops-send-btn">전송</button>
|
|
425
|
+
</form>
|
|
426
|
+
</aside>
|
|
427
|
+
</div>
|
|
428
|
+
<button class="ops-reopen" onclick="toggleOps()">💬 Ops Thread 열기</button>
|
|
429
|
+
<script>
|
|
430
|
+
const state={tasks:[],agents:[],filter:'all',agentFilter:null,priorityFilter:null,search:'',project:'apex',collapsed:true};
|
|
431
|
+
function getCol(t){const s=(t.status||'pending').toLowerCase();if(s.includes('progress')||s==='running')return 'in_progress';if(s.includes('review'))return 'in_review';if(s==='done'||s==='completed')return 'completed';return 'pending';}
|
|
432
|
+
function statusLabel(c){return{pending:'대기',in_progress:'진행중',in_review:'사람검토',completed:'완료'}[c]||c}
|
|
433
|
+
function escapeHtml(s){return String(s||'').replace(/[&<>"']/g,c=>({"&":"&","<":"<",">":">",'"':""","'":"'"}[c]))}
|
|
434
|
+
async function loadAll(){await Promise.all([loadAgents(),loadTasks()])}
|
|
435
|
+
async function loadAgents(){const r=await fetch(`/api/agents?project=${state.project}`);const j=await r.json();state.agents=j.agents||[]}
|
|
436
|
+
async function loadTasks(){const r=await fetch(`/api/tasks?project=${state.project}`);state.tasks=await r.json();renderAll()}
|
|
437
|
+
function renderAll(){renderAgents();renderBoard();updateStats();updateFilterCounts()}
|
|
438
|
+
function updateFilterCounts(){
|
|
439
|
+
const t=state.tasks;
|
|
440
|
+
document.getElementById('fc-all').textContent=t.length;
|
|
441
|
+
document.getElementById('fc-ops').textContent=t.filter(x=>{const a=state.agents.find(g=>g.name===x.agent);return a&&a.group==='opsguard'}).length;
|
|
442
|
+
document.getElementById('fc-exam').textContent=t.filter(x=>{const a=state.agents.find(g=>g.name===x.agent);return a&&a.group==='examguard'}).length;
|
|
443
|
+
document.getElementById('fc-cv').textContent=t.filter(x=>{const r=(x.metadata&&x.metadata.runner)||'';return r==='both'||r.startsWith('reviewer:')}).length;
|
|
444
|
+
document.getElementById('fc-par').textContent=t.filter(x=>!x.parentId).length;
|
|
445
|
+
document.getElementById('fc-incident').textContent=t.filter(x=>x.metadata&&x.metadata.source==='incident-response').length;
|
|
446
|
+
}
|
|
447
|
+
function renderAgents(){
|
|
448
|
+
const ops=state.agents.filter(a=>a.group==='opsguard');
|
|
449
|
+
const exam=state.agents.filter(a=>a.group==='examguard');
|
|
450
|
+
document.getElementById('ag-rows-ops').innerHTML=ops.map(agentRowHTML).join('');
|
|
451
|
+
document.getElementById('ag-rows-exam').innerHTML=exam.map(agentRowHTML).join('');
|
|
452
|
+
const opsBusy=ops.reduce((n,a)=>n+state.tasks.filter(t=>t.agent===a.name&&getCol(t)==='in_progress').length,0);
|
|
453
|
+
const examBusy=exam.reduce((n,a)=>n+state.tasks.filter(t=>t.agent===a.name&&getCol(t)==='in_progress').length,0);
|
|
454
|
+
document.getElementById('ag-meta-ops').textContent=`${ops.length} agents · ${opsBusy} active`;
|
|
455
|
+
document.getElementById('ag-meta-exam').textContent=`${exam.length} agents · ${examBusy} active`;
|
|
456
|
+
document.querySelectorAll('.ag-row').forEach(el=>{
|
|
457
|
+
el.onclick=()=>{const name=el.dataset.agent;state.agentFilter=state.agentFilter===name?null:name;document.querySelectorAll('.ag-row').forEach(r=>r.classList.toggle('active',r.dataset.agent===state.agentFilter));renderAgentTag();renderBoard();};
|
|
458
|
+
});
|
|
459
|
+
if(state.agentFilter)document.querySelector(`.ag-row[data-agent="${state.agentFilter}"]`)?.classList.add('active');
|
|
460
|
+
}
|
|
461
|
+
function renderAgentTag(){
|
|
462
|
+
const wrap=document.getElementById('agent-tag-wrap');
|
|
463
|
+
wrap.innerHTML=state.agentFilter?`<div class="f-tag">⊙ ${escapeHtml(state.agentFilter)}<span class="x" onclick="clearAgentFilter()">✕</span></div>`:'';
|
|
464
|
+
}
|
|
465
|
+
function clearAgentFilter(){state.agentFilter=null;document.querySelectorAll('.ag-row').forEach(c=>c.classList.remove('active'));renderAgentTag();renderBoard();}
|
|
466
|
+
function agentRowHTML(a){
|
|
467
|
+
const color=a.color||'#71717a';const runner=a.runner||'—';const rcls='r-'+(runner||'').replace(/[^a-z0-9]/gi,'-').toLowerCase();const role=a.role||'';
|
|
468
|
+
const busy=state.tasks.filter(t=>t.agent===a.name&&getCol(t)==='in_progress').length;const total=state.tasks.filter(t=>t.agent===a.name).length;const isActive=state.agentFilter===a.name?' active':'';
|
|
469
|
+
return `<div class="ag-row${isActive}" data-agent="${escapeHtml(a.name)}"><span class="ag-dot" style="background:${color}"></span><span class="ag-name">${escapeHtml(a.name)}</span><span class="ag-role ag-role-cell">${escapeHtml(role)}</span><span class="ag-runner ${rcls} ag-runner-cell">${escapeHtml(runner)}</span><span class="ag-load ${busy>0?'busy':''}"><span class="lp"></span>${busy}/${total}</span><button class="ag-detail-btn" onclick="event.stopPropagation();openAgentModal('${escapeHtml(a.name)}')">상세</button></div>`;
|
|
470
|
+
}
|
|
471
|
+
function applyFilters(tasks){
|
|
472
|
+
let v=tasks.slice();
|
|
473
|
+
if(state.agentFilter)v=v.filter(t=>t.agent===state.agentFilter);
|
|
474
|
+
if(state.filter==='opsguard'||state.filter==='examguard'){v=v.filter(t=>{const a=state.agents.find(x=>x.name===t.agent);return a&&a.group===state.filter});}
|
|
475
|
+
else if(state.filter==='cross-validated'){v=v.filter(t=>{const r=(t.metadata&&t.metadata.runner)||'';return r==='both'||r.startsWith('reviewer:')});}
|
|
476
|
+
else if(state.filter==='parents-only'){v=v.filter(t=>!t.parentId);}
|
|
477
|
+
else if(state.filter==='incident'){v=v.filter(t=>t.metadata&&t.metadata.source==='incident-response');}
|
|
478
|
+
if(state.priorityFilter)v=v.filter(t=>(t.priority||'medium')===state.priorityFilter);
|
|
479
|
+
if(state.search){const q=state.search.toLowerCase();v=v.filter(t=>String(t.subject||'').toLowerCase().includes(q)||String(t.id||'').includes(q)||String(t.agent||'').toLowerCase().includes(q));}
|
|
480
|
+
return v;
|
|
481
|
+
}
|
|
482
|
+
function renderBoard(){
|
|
483
|
+
const v=applyFilters(state.tasks);const cols={pending:[],in_progress:[],in_review:[],completed:[]};v.forEach(t=>cols[getCol(t)].push(t));let total=0;
|
|
484
|
+
const pmap={critical:0,high:1,medium:2,low:3};
|
|
485
|
+
for(const c of ['pending','in_progress','in_review','completed']){
|
|
486
|
+
const w=document.getElementById(`col-${c}`);document.getElementById(`cn-${c}`).textContent=cols[c].length;total+=cols[c].length;
|
|
487
|
+
if(cols[c].length===0){w.innerHTML='<div class="col-empty">비어 있음</div>';continue}
|
|
488
|
+
cols[c].sort((a,b)=>{const pa=pmap[a.priority||'medium']??2;const pb=pmap[b.priority||'medium']??2;if(pa!==pb)return pa-pb;return Number(a.id)-Number(b.id);});
|
|
489
|
+
w.innerHTML=cols[c].map(taskCardHTML).join('');
|
|
490
|
+
}
|
|
491
|
+
const vcnt=document.getElementById('visible-cnt');if(vcnt)vcnt.textContent=total+' 표시중';
|
|
492
|
+
}
|
|
493
|
+
function taskCardHTML(t){
|
|
494
|
+
const agent=state.agents.find(a=>a.name===t.agent);const agentColor=agent?.color||'#a1a1aa';const agentLabel=agent?.name||t.agent||'—';
|
|
495
|
+
const runner=(t.metadata&&t.metadata.runner)||'';const cv=(t.metadata&&t.metadata.crossValidation)||t.crossValidation;const agreement=cv?.agreement||null;
|
|
496
|
+
const isSub=!!t.parentId;const dis=agreement==='disagreed';const priority=t.priority||'medium';const isIncident=(t.metadata&&t.metadata.source==='incident-response');
|
|
497
|
+
const tags=[];if(isIncident)tags.push({label:'incident',cls:'tag-incident'});
|
|
498
|
+
if(t.tags){(Array.isArray(t.tags)?t.tags:[t.tags]).forEach(tag=>{if(!tag)return;const tl=String(tag).toLowerCase();const cls=tl.includes('security')?'tag-security':tl.includes('harness')?'tag-harness':tl.includes('content')?'tag-content':'tag-default';tags.push({label:tag,cls});});}
|
|
499
|
+
const kids=state.tasks.filter(x=>x.parentId===t.id);const kdone=kids.filter(c=>getCol(c)==='completed').length;const kinp=kids.filter(c=>getCol(c)==='in_progress').length;const kt=kids.length;const kpct=kt?Math.round(kdone/kt*100):0;
|
|
500
|
+
return `<div class="tc${isSub?' subtask':''}${dis?' disagreed':''}" data-id="${t.id}" style="border-left-color:${dis?'var(--st-flag-dot)':agentColor}">
|
|
501
|
+
<div class="tc-top"><span class="tc-id">#${t.id}${isSub?`<span class="pref"> ←#${t.parentId}</span>`:''}</span><span class="tc-pri pri-${priority}">${priority}</span>${tags.map(tg=>`<span class="tc-tag ${tg.cls}">${escapeHtml(tg.label)}</span>`).join('')}</div>
|
|
502
|
+
<div class="tc-title">${escapeHtml(t.subject||'Untitled')}</div>
|
|
503
|
+
<div class="tc-meta"><span class="tc-ag"><span class="agd" style="background:${agentColor}"></span>${escapeHtml(agentLabel)}</span>${runner?`<span class="tc-rn">${renderRunner(runner)}</span>`:''}${agreement?`<span class="tc-agree ${agreement}">${agreement==='agreed'?'✓':agreement==='disagreed'?'✗':'⋯'}</span>`:''}</div>
|
|
504
|
+
${kt>0?`<div class="tc-prog"><div class="fill${kpct===100?' done':''}" style="width:${kpct}%"></div></div><div class="tc-sub"><span>${kdone}/${kt}${kinp?` · ${kinp}진행`:''}</span><span>${kpct}%</span></div>`:''}
|
|
505
|
+
</div>`;
|
|
506
|
+
}
|
|
507
|
+
function renderRunner(r){
|
|
508
|
+
if(r==='claude')return '<span class="mc claude">claude</span>';if(r==='codex')return '<span class="mc codex">codex</span>';
|
|
509
|
+
if(r==='both')return '<span class="mc claude">claude</span><span class="arr">⇄</span><span class="mc codex">codex</span>';
|
|
510
|
+
if(r==='reviewer:codex')return '<span class="mc claude">claude</span><span class="arr">→</span><span class="mc codex">codex</span>';
|
|
511
|
+
if(r==='reviewer:claude')return '<span class="mc codex">codex</span><span class="arr">→</span><span class="mc claude">claude</span>';
|
|
512
|
+
return escapeHtml(r);
|
|
513
|
+
}
|
|
514
|
+
function updateStats(){const t=state.tasks;document.getElementById('s-total').textContent=t.length;document.getElementById('s-active').textContent=t.filter(x=>getCol(x)==='in_progress').length;document.getElementById('s-review').textContent=t.filter(x=>getCol(x)==='in_review').length;document.getElementById('s-done').textContent=t.filter(x=>getCol(x)==='completed').length;}
|
|
515
|
+
function setFilter(f){state.filter=f;document.querySelectorAll('.f-pill').forEach(b=>b.classList.toggle('active',b.dataset.flt===f));renderBoard();}
|
|
516
|
+
function setPriority(p){state.priorityFilter=p==='all'?null:p;document.querySelectorAll('.pri-pill').forEach(b=>b.classList.toggle('active',b.dataset.pri===(p||'all')));renderBoard();}
|
|
517
|
+
function setSearch(q){state.search=q.trim();renderBoard();}
|
|
518
|
+
function toggleAgents(){state.collapsed=!state.collapsed;document.body.classList.toggle('agents-collapsed',state.collapsed);const t=document.querySelector('.agents-panel-head .toggle');if(t)t.textContent=state.collapsed?'펼치기 ▼':'접기 ▲';document.getElementById('ag-toggle-btn').textContent=state.collapsed?'에이전트':'에이전트 접기';}
|
|
519
|
+
function toggleTheme(){const cur=document.body.dataset.theme;const next=cur==='light'?'dark':'light';document.body.dataset.theme=next;document.getElementById('theme-tog').textContent=next==='light'?'☾':'☀';localStorage.setItem('sentinel-theme',next);}
|
|
520
|
+
(function initTheme(){const t=localStorage.getItem('sentinel-theme')||'light';document.body.dataset.theme=t;document.getElementById('theme-tog').textContent=t==='light'?'☾':'☀'})();
|
|
521
|
+
let modalTaskId=null;
|
|
522
|
+
function openTaskModal(id){
|
|
523
|
+
const t=state.tasks.find(x=>String(x.id)===String(id));if(!t)return;modalTaskId=String(id);
|
|
524
|
+
const col=getCol(t);const agent=state.agents.find(a=>a.name===t.agent);const agentColor=agent?.color||'#71717a';
|
|
525
|
+
document.getElementById('t-id').textContent='#'+t.id+(t.parentId?` (← #${t.parentId})`:'');
|
|
526
|
+
document.getElementById('t-agent-dot').style.background=agentColor;
|
|
527
|
+
document.getElementById('t-agent-tag').textContent=t.agent||'unassigned';
|
|
528
|
+
const sp=document.getElementById('t-status-pill');sp.dataset.s=col;sp.innerHTML=`<span class="sd"></span>${statusLabel(col)}`;
|
|
529
|
+
document.getElementById('t-title-h').textContent=t.subject||'Untitled';
|
|
530
|
+
const cv=(t.metadata&&t.metadata.crossValidation)||t.crossValidation;const cvBar=document.getElementById('t-cv-bar');
|
|
531
|
+
if(cv){const a=cv.agreement||'pending';const labels={agreed:'✓ Claude·Codex 일치',disagreed:'✗ Claude·Codex 불일치 — 사람 검토 필요',pending:'⋯ Cross-validation 대기 중'};const colors={agreed:'var(--st-pass-fg)',disagreed:'var(--st-flag-fg)',pending:'var(--c-text-3)'};const bgs={agreed:'var(--st-pass-bg)',disagreed:'var(--st-flag-bg)',pending:'var(--c-surface-2)'};cvBar.style.display='block';cvBar.style.color=colors[a];cvBar.style.background=bgs[a];cvBar.textContent=labels[a];}else{cvBar.style.display='none';}
|
|
532
|
+
document.getElementById('t-status').value=col;document.getElementById('t-runner').value=(t.metadata&&t.metadata.runner)||'';document.getElementById('t-desc').value=t.description||'';document.getElementById('t-report').value=t.reportSummary||'';
|
|
533
|
+
const agSel=document.getElementById('t-agent');agSel.innerHTML='<option value="">— 미지정 —</option>'+state.agents.filter(a=>a.group==='opsguard').map(a=>`<option value="${a.name}">[Ops] ${a.name}${a.role?' — '+a.role:''}</option>`).join('')+state.agents.filter(a=>a.group==='examguard').map(a=>`<option value="${a.name}">[Exam] ${a.name}${a.role?' — '+a.role:''}</option>`).join('')+state.agents.filter(a=>!a.group).map(a=>`<option value="${a.name}">${a.name}</option>`).join('');agSel.value=t.agent||'';
|
|
534
|
+
const rel=document.getElementById('t-related');let relHTML='';
|
|
535
|
+
if(t.parentId){const p=state.tasks.find(x=>String(x.id)===String(t.parentId));if(p)relHTML+=`<div style="font-size:11px;color:var(--c-text-3)">상위 Phase</div><div style="padding:8px 10px;background:var(--c-surface-2);border-radius:6px;cursor:pointer;font-size:12px" onclick="openTaskModal('${p.id}')"><strong style="font-family:'JetBrains Mono',monospace">#${p.id}</strong> ${escapeHtml(p.subject)}</div>`;}
|
|
536
|
+
const kids=state.tasks.filter(x=>String(x.parentId)===String(t.id));
|
|
537
|
+
if(kids.length>0){const kdone=kids.filter(c=>getCol(c)==='completed').length;relHTML+=`<div style="font-size:11px;color:var(--c-text-3);margin-top:6px">하위 작업 (${kdone}/${kids.length})</div><div style="display:flex;flex-direction:column;gap:4px">`+kids.map(c=>{const ccol=getCol(c);const ca=state.agents.find(a=>a.name===c.agent);const ccolor=ca?.color||'#a1a1aa';return `<div style="padding:6px 10px;background:var(--c-surface-2);border-radius:5px;cursor:pointer;font-size:11.5px;display:flex;align-items:center;gap:8px;border-left:3px solid ${ccolor}" onclick="openTaskModal('${c.id}')"><span class="tc-st" data-s="${ccol}"><span class="sd"></span>${statusLabel(ccol)}</span><strong style="font-family:'JetBrains Mono',monospace">#${c.id}</strong><span style="flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${escapeHtml(c.subject)}</span></div>`;}).join('')+'</div>';}
|
|
538
|
+
rel.innerHTML=relHTML;
|
|
539
|
+
const tl=document.getElementById('t-timeline');const times=[];
|
|
540
|
+
if(t.createdAt)times.push(`Created ${new Date(t.createdAt).toLocaleString('ko-KR')}`);if(t.startedAt)times.push(`Started ${new Date(t.startedAt).toLocaleString('ko-KR')}`);if(t.completedAt)times.push(`Completed ${new Date(t.completedAt).toLocaleString('ko-KR')}`);if(t.updatedAt&&t.updatedAt!==t.createdAt)times.push(`Updated ${new Date(t.updatedAt).toLocaleString('ko-KR')}`);
|
|
541
|
+
if(times.length){tl.style.display='block';tl.innerHTML=times.join('<br>');}else{tl.style.display='none';}
|
|
542
|
+
document.getElementById('t-meta-info').textContent=`priority: ${t.priority||'medium'} · session: ${(t._session||'kanban').slice(0,12)}`;
|
|
543
|
+
document.getElementById('t-delete').style.display=t._editable===false?'none':'inline-block';
|
|
544
|
+
document.getElementById('task-modal').classList.add('open');
|
|
545
|
+
}
|
|
546
|
+
function closeTaskModal(){document.getElementById('task-modal').classList.remove('open');modalTaskId=null;}
|
|
547
|
+
async function saveTaskModal(){
|
|
548
|
+
if(!modalTaskId)return;const btn=document.getElementById('t-save');btn.disabled=true;btn.textContent='저장 중...';
|
|
549
|
+
const status=document.getElementById('t-status').value;const agent=document.getElementById('t-agent').value;const runner=document.getElementById('t-runner').value;const desc=document.getElementById('t-desc').value;const report=document.getElementById('t-report').value.trim();
|
|
550
|
+
const payload={status,agent,description:desc};if(report)payload.reportSummary=report;if(runner)payload.metadata={runner};
|
|
551
|
+
try{const r=await fetch(`/api/tasks/${modalTaskId}`,{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)});if(!r.ok)throw new Error('save failed');showToast(`#${modalTaskId} 저장 완료`,'ok');closeTaskModal();await loadTasks();}catch(e){showToast('저장 실패: '+e.message,'err');}finally{btn.disabled=false;btn.textContent='저장';}
|
|
552
|
+
}
|
|
553
|
+
async function deleteTaskFromModal(){
|
|
554
|
+
if(!modalTaskId)return;if(!confirm(`#${modalTaskId} 카드를 삭제할까요?`))return;
|
|
555
|
+
try{const r=await fetch(`/api/tasks/${modalTaskId}`,{method:'DELETE'});if(!r.ok&&r.status!==204)throw new Error('delete failed');showToast(`#${modalTaskId} 삭제됨`,'ok');closeTaskModal();await loadTasks();}catch(e){showToast('삭제 실패: '+e.message,'err');}
|
|
556
|
+
}
|
|
557
|
+
let modalCurrentAgent=null;
|
|
558
|
+
async function openAgentModal(name){
|
|
559
|
+
modalCurrentAgent=name;const overlay=document.getElementById('agent-modal');const a=state.agents.find(x=>x.name===name)||{};
|
|
560
|
+
document.getElementById('m-title').textContent=name;document.getElementById('m-dot').style.background=a.color||'#71717a';document.getElementById('m-group').textContent=a.group||'—';
|
|
561
|
+
try{const r=await fetch(`/api/agents/${encodeURIComponent(name)}/full`);if(!r.ok)throw new Error('failed to load');const j=await r.json();const m=j.meta||{};document.getElementById('f-mission').value=m.mission||'';document.getElementById('f-role').value=m.role||a.role||'';document.getElementById('f-runner').value=m.runner||a.runner||'claude';document.getElementById('f-group').value=m.group||a.group||'opsguard';const color=m.color||a.color||'#71717a';document.getElementById('f-color').value=color;document.getElementById('f-color-hex').value=color;document.getElementById('f-body').value=j.body||'';document.getElementById('f-changenote').value='';document.getElementById('m-path').textContent=j.path||'';}catch(e){showToast('로딩 실패: '+e.message,'err');}
|
|
562
|
+
overlay.classList.add('open');
|
|
563
|
+
document.getElementById('f-color').oninput=(e)=>document.getElementById('f-color-hex').value=e.target.value;
|
|
564
|
+
document.getElementById('f-color-hex').oninput=(e)=>{const v=e.target.value;if(/^#[0-9a-f]{6}$/i.test(v))document.getElementById('f-color').value=v;};
|
|
565
|
+
}
|
|
566
|
+
function closeAgentModal(){document.getElementById('agent-modal').classList.remove('open');modalCurrentAgent=null;}
|
|
567
|
+
async function saveAgentModal(){
|
|
568
|
+
if(!modalCurrentAgent)return;const saveBtn=document.getElementById('m-save');saveBtn.disabled=true;saveBtn.textContent='저장 중...';
|
|
569
|
+
const payload={meta:{mission:document.getElementById('f-mission').value.trim(),role:document.getElementById('f-role').value.trim(),runner:document.getElementById('f-runner').value,group:document.getElementById('f-group').value,color:document.getElementById('f-color-hex').value||document.getElementById('f-color').value},body:document.getElementById('f-body').value,changeNote:document.getElementById('f-changenote').value.trim()||undefined};
|
|
570
|
+
try{const r=await fetch(`/api/agents/${encodeURIComponent(modalCurrentAgent)}`,{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)});if(!r.ok)throw new Error((await r.json()).error||'save failed');showToast(`${modalCurrentAgent} 저장 완료`,'ok');closeAgentModal();await loadAgents();renderAll();}catch(e){showToast('저장 실패: '+e.message,'err');}finally{saveBtn.disabled=false;saveBtn.textContent='저장';}
|
|
571
|
+
}
|
|
572
|
+
function showToast(msg,type){let c=document.getElementById('toast-stack');if(!c){c=document.createElement('div');c.id='toast-stack';c.style.cssText='position:fixed;bottom:20px;right:20px;z-index:300;display:flex;flex-direction:column;gap:8px;pointer-events:none';document.body.appendChild(c);}const t=document.createElement('div');t.textContent=msg;t.style.cssText=`padding:9px 14px;border-radius:8px;font-size:12px;font-weight:600;color:#fff;box-shadow:0 4px 12px rgba(0,0,0,0.2);max-width:300px;background:${type==='ok'?'#059669':type==='err'?'#dc2626':'#27272a'};pointer-events:auto`;c.appendChild(t);setTimeout(()=>{t.style.opacity='0';t.style.transition='opacity 0.3s';setTimeout(()=>t.remove(),300);},2500);}
|
|
573
|
+
document.addEventListener('keydown',(e)=>{if(e.key==='Escape'){if(document.getElementById('task-modal').classList.contains('open'))closeTaskModal();else if(document.getElementById('agent-modal').classList.contains('open'))closeAgentModal();}});
|
|
574
|
+
document.addEventListener('click',(e)=>{const card=e.target.closest('.tc');if(!card)return;const sel=window.getSelection&&window.getSelection().toString();if(sel&&sel.length>0)return;const id=card.dataset.id;if(id)openTaskModal(id);});
|
|
575
|
+
function connectSSE(){const es=new EventSource('/events');const conn=document.getElementById('conn');const lbl=document.getElementById('conn-l');let rt=null;es.onopen=()=>{conn.dataset.s='connected';lbl.textContent='연결됨'};es.onmessage=(e)=>{try{const d=JSON.parse(e.data);if(d.type==='ops.message'&&d.message){appendOpsMsg(d.message);return}if(d.tasks){state.tasks=d.tasks.filter(t=>t.project===state.project||(!t.project&&t._session==='kanban'));clearTimeout(rt);rt=setTimeout(renderAll,250)}}catch(ex){console.warn(ex)}};es.onerror=()=>{conn.dataset.s='disconnected';lbl.textContent='재연결중';es.close();setTimeout(connectSSE,3000)};}
|
|
576
|
+
|
|
577
|
+
// ── Ops Thread (Telegram mirror) ───────────────────────────────────────────
|
|
578
|
+
const ops={msgs:[],lastId:null,collapsed:localStorage.getItem('kbn-ops-collapsed')==='1'};
|
|
579
|
+
function linkifyTaskRefs(text){
|
|
580
|
+
const esc=escapeHtml(text);
|
|
581
|
+
return esc.replace(/#(\d+)/g,(m,n)=>`<span class="tag" data-tid="${n}" onclick="openTaskModal('${n}')">#${n}</span>`);
|
|
582
|
+
}
|
|
583
|
+
function renderOpsMsg(m){
|
|
584
|
+
if(m.role==='system'){
|
|
585
|
+
return `<div class="ops-msg system">${linkifyTaskRefs(m.text||'')}</div>`;
|
|
586
|
+
}
|
|
587
|
+
const cls=['ops-msg',escapeHtml(m.role||'system')].join(' ');
|
|
588
|
+
const time=m.ts?new Date(m.ts).toLocaleTimeString('ko-KR',{hour:'2-digit',minute:'2-digit'}):'';
|
|
589
|
+
const src=m.source==='telegram'?' · tg':'';
|
|
590
|
+
const tag=m.taskId?` · #${escapeHtml(m.taskId)}`:'';
|
|
591
|
+
return `<div class="${cls}">${linkifyTaskRefs(m.text||'')}<div class="meta">${time}${tag}${src}</div></div>`;
|
|
592
|
+
}
|
|
593
|
+
function renderOpsAll(){
|
|
594
|
+
const c=document.getElementById('ops-msgs');
|
|
595
|
+
if(!ops.msgs.length){c.innerHTML=`<div class="ops-empty">메시지 없음.<br><br>입력창에 메시지를 보내면<br>여기와 텔레그램 양쪽에 기록됩니다.<br><br><kbd>Enter</kbd> 전송 · <kbd>Shift+Enter</kbd> 줄바꿈</div>`;return}
|
|
596
|
+
c.innerHTML=ops.msgs.map(renderOpsMsg).join('');
|
|
597
|
+
c.scrollTop=c.scrollHeight;
|
|
598
|
+
}
|
|
599
|
+
function appendOpsMsg(m){
|
|
600
|
+
if(!m||!m.id)return;if(ops.msgs.some(x=>x.id===m.id))return;
|
|
601
|
+
ops.msgs.push(m);ops.lastId=m.id;
|
|
602
|
+
if(ops.msgs.length>500)ops.msgs=ops.msgs.slice(-500);
|
|
603
|
+
const empty=document.querySelector('#ops-msgs .ops-empty');if(empty)empty.remove();
|
|
604
|
+
const c=document.getElementById('ops-msgs');c.insertAdjacentHTML('beforeend',renderOpsMsg(m));c.scrollTop=c.scrollHeight;
|
|
605
|
+
}
|
|
606
|
+
async function loadOpsThread(){
|
|
607
|
+
try{const r=await fetch('/api/ops-thread');if(!r.ok)return;ops.msgs=await r.json();if(ops.msgs.length)ops.lastId=ops.msgs[ops.msgs.length-1].id;renderOpsAll();}catch{}
|
|
608
|
+
}
|
|
609
|
+
async function refreshTelegramState(){
|
|
610
|
+
try{const r=await fetch('/api/telegram/status');if(!r.ok)return;const j=await r.json();const ttl=document.getElementById('ops-ttl');const sub=document.getElementById('ops-tg-state');if(j.configured){ttl.classList.add('connected');sub.textContent=j.polling?'tg · '+j.chatId:'tg paused';}else{ttl.classList.remove('connected');sub.textContent='local';}}catch{}
|
|
611
|
+
}
|
|
612
|
+
async function sendOps(ev){
|
|
613
|
+
if(ev&&ev.preventDefault)ev.preventDefault();
|
|
614
|
+
const ta=document.getElementById('ops-input');const btn=document.getElementById('ops-send-btn');const text=(ta.value||'').trim();if(!text)return false;
|
|
615
|
+
btn.disabled=true;ta.disabled=true;
|
|
616
|
+
try{const r=await fetch('/api/ops-thread/send',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({text})});if(!r.ok)throw new Error('send failed');ta.value='';autoGrowOps(ta);}
|
|
617
|
+
catch(e){showToast('전송 실패: '+e.message,'err');}
|
|
618
|
+
finally{btn.disabled=false;ta.disabled=false;ta.focus();}
|
|
619
|
+
return false;
|
|
620
|
+
}
|
|
621
|
+
function opsKey(e){if(e.key==='Enter'&&!e.shiftKey){e.preventDefault();sendOps();}}
|
|
622
|
+
function autoGrowOps(ta){ta.style.height='32px';ta.style.height=Math.min(120,ta.scrollHeight)+'px';}
|
|
623
|
+
function toggleOps(){ops.collapsed=!ops.collapsed;document.body.classList.toggle('ops-collapsed',ops.collapsed);localStorage.setItem('kbn-ops-collapsed',ops.collapsed?'1':'0');if(!ops.collapsed){setTimeout(()=>{const c=document.getElementById('ops-msgs');c.scrollTop=c.scrollHeight;},200);}}
|
|
624
|
+
(function initOps(){if(ops.collapsed)document.body.classList.add('ops-collapsed');})();
|
|
625
|
+
(async function init(){if(state.collapsed){document.body.classList.add('agents-collapsed');const t=document.querySelector('.agents-panel-head .toggle');if(t)t.textContent='펼치기 ▼';document.getElementById('ag-toggle-btn').textContent='에이전트';}await loadAll();await loadOpsThread();refreshTelegramState();setInterval(refreshTelegramState,30000);connectSSE();})();
|
|
626
|
+
</script>
|
|
627
|
+
</body>
|
|
628
|
+
</html>
|