kodelyth-ecc 1.5.9 → 1.7.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/.github/workflows/publish.yml +19 -0
- package/AGENTS.md +1 -1
- package/CHANGELOG.md +455 -0
- package/CLAUDE.md +48 -16
- package/README.md +265 -81
- package/VERSION +1 -1
- package/actions/ecc-review/README.md +243 -0
- package/actions/ecc-review/action.yml +158 -0
- package/actions/ecc-review/post-comment.js +171 -0
- package/actions/ecc-review/run-review.js +285 -0
- package/agents/backdoor-hunter.md +260 -0
- package/agents/chaos-engineer.md +251 -0
- package/agents/code-stealer-detector.md +228 -0
- package/agents/jailbreak-tester.md +222 -0
- package/agents/license-violation-finder.md +212 -0
- package/agents/prompt-injection-hunter.md +126 -0
- package/agents/secret-hunter.md +209 -0
- package/agents/supply-chain-auditor.md +195 -0
- package/bin/kodelyth-ecc.js +875 -1
- package/bundles/enterprise.md +172 -0
- package/bundles/indie-hacker.md +106 -0
- package/bundles/red-team.md +138 -0
- package/cat +0 -0
- package/commands/dashboard.md +67 -0
- package/commands/devil-mode.md +121 -0
- package/commands/memory-evolve.md +71 -0
- package/commands/replay.md +61 -0
- package/commands/route-model.md +48 -0
- package/commands/swarm.md +68 -0
- package/commands/verify-supply-chain.md +59 -0
- package/docs/dashboard.md +211 -0
- package/docs/evolve.md +303 -0
- package/docs/mcp-clients.md +167 -0
- package/docs/mcp.md +178 -0
- package/docs/replay.md +244 -0
- package/docs/supply-chain.md +207 -0
- package/docs/swarm.md +243 -0
- package/hooks/hooks.json +52 -0
- package/hooks/memory/auto-recall.js +29 -1
- package/hooks/safety/README.md +124 -0
- package/hooks/safety/lib/patterns.js +179 -0
- package/hooks/safety/prompt-injection-guard.js +179 -0
- package/hooks/safety/token-budget.js +229 -0
- package/install.ps1 +199 -2
- package/install.sh +286 -4
- package/package.json +30 -4
- package/rules/common/agent-intent-routing.md +182 -0
- package/rules/common/cost-aware-model-routing.md +152 -0
- package/scripts/dashboard/data.js +353 -0
- package/scripts/dashboard/server.js +324 -0
- package/scripts/dashboard/static/index.html +645 -0
- package/scripts/evolve/analyze.js +303 -0
- package/scripts/evolve/proposals.js +162 -0
- package/scripts/evolve/stats.js +219 -0
- package/scripts/mcp/catalog.js +244 -0
- package/scripts/mcp/client.js +181 -0
- package/scripts/mcp/prompts.js +133 -0
- package/scripts/mcp/resources.js +94 -0
- package/scripts/mcp/server.js +153 -0
- package/scripts/mcp/tools.js +465 -0
- package/scripts/replay/bundle.js +191 -0
- package/scripts/replay/replay.js +107 -0
- package/scripts/router/classify.js +232 -0
- package/scripts/supply-chain/manifest.js +154 -0
- package/scripts/supply-chain/sbom.js +202 -0
- package/scripts/supply-chain/verify.js +102 -0
- package/scripts/swarm/build-plan.js +193 -0
- package/skills/cost-aware-model-routing/SKILL.md +153 -0
- package/skills/kodelyth-quickstart/SKILL.md +7 -0
- package/skills/observability-dashboard/SKILL.md +119 -0
- package/skills/self-evolving-memory/SKILL.md +175 -0
- package/skills/session-replay/SKILL.md +199 -0
- package/skills/supply-chain-verification/SKILL.md +201 -0
- package/skills/swarm-orchestrator/SKILL.md +177 -0
- package/social/card-install.svg +1 -1
- package/social/facebook-group/POST.md +121 -0
- package/social/facebook-group/fb-1-3am-debug.png +0 -0
- package/social/facebook-group/fb-1-3am-debug.svg +97 -0
- package/social/facebook-group/fb-2-cpu-upgrade.png +0 -0
- package/social/facebook-group/fb-2-cpu-upgrade.svg +132 -0
- package/social/facebook-group/fb-3-before-after.png +0 -0
- package/social/facebook-group/fb-3-before-after.svg +94 -0
- package/social/facebook-v150.svg +5 -5
- package/social/github-social-preview.svg +119 -100
- package/social/readme-hero.svg +11 -11
- package/social/section-agents.svg +57 -0
- package/social/section-author.svg +54 -0
- package/social/section-dashboard.svg +59 -0
- package/social/section-devil.svg +54 -0
- package/social/section-hooks.svg +51 -0
- package/social/section-install.svg +42 -0
- package/social/section-learning.svg +52 -0
- package/social/section-mcp.svg +46 -0
- package/social/section-memory.svg +57 -0
- package/social/section-parallel.svg +72 -0
- package/social/section-routing.svg +51 -0
- package/social/x-card-agents-grid.svg +6 -6
- package/tests/dashboard/data.test.js +235 -0
- package/tests/dashboard/server.test.js +240 -0
- package/tests/evolve/analyze.test.js +169 -0
- package/tests/evolve/proposals.test.js +173 -0
- package/tests/evolve/stats.test.js +159 -0
- package/tests/mcp/catalog.test.js +98 -0
- package/tests/mcp/client.test.js +109 -0
- package/tests/mcp/resources-prompts.test.js +70 -0
- package/tests/mcp/tools.test.js +159 -0
- package/tests/replay/bundle.test.js +181 -0
- package/tests/replay/replay.test.js +131 -0
- package/tests/router/classify.test.js +164 -0
- package/tests/safety/patterns.test.js +94 -0
- package/tests/safety/prompt-injection-guard.test.js +111 -0
- package/tests/safety/token-budget.test.js +119 -0
- package/tests/supply-chain/manifest.test.js +147 -0
- package/tests/supply-chain/sbom.test.js +170 -0
- package/tests/supply-chain/verify.test.js +146 -0
- package/tests/swarm/build-plan.test.js +188 -0
- package/wiki/Agent-Reference.md +58 -7
- package/wiki/FAQ.md +204 -7
- package/wiki/Home.md +104 -28
- package/wiki/Hook-Reference.md +1 -1
- package/wiki/Installation-Guide.md +109 -6
- package/wiki/Platform-Support.md +239 -25
- package/wiki/Skill-Reference.md +101 -6
|
@@ -0,0 +1,645 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>Kodelyth ECC — Dashboard</title>
|
|
7
|
+
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><defs><linearGradient id='g' x1='0' y1='0' x2='1' y2='1'><stop offset='0' stop-color='%23111a2e'/><stop offset='1' stop-color='%230b1220'/></linearGradient></defs><rect width='32' height='32' rx='7' fill='url(%23g)' stroke='%231f2a44' stroke-width='1'/><path d='M10.5 7.5 L10.5 24.5 M10.5 16 L21 7.5 M14 13.5 L21.5 24.5' stroke='%23e5e7eb' stroke-width='2.6' stroke-linecap='round' stroke-linejoin='round' fill='none'/><circle cx='25' cy='7' r='3' fill='%2334d399'/></svg>">
|
|
8
|
+
<style>
|
|
9
|
+
/* ───── reset + base ───── */
|
|
10
|
+
*, *::before, *::after { box-sizing: border-box; }
|
|
11
|
+
html, body { margin: 0; padding: 0; height: 100%; }
|
|
12
|
+
body {
|
|
13
|
+
font: 14px/1.5 ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
14
|
+
color: #e5e7eb;
|
|
15
|
+
background: radial-gradient(1200px 600px at 0% 0%, #1e293b 0%, #0b1220 70%, #060912 100%) fixed;
|
|
16
|
+
min-height: 100vh;
|
|
17
|
+
}
|
|
18
|
+
a { color: #93c5fd; text-decoration: none; }
|
|
19
|
+
a:hover { text-decoration: underline; }
|
|
20
|
+
code, pre, kbd, samp {
|
|
21
|
+
font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
|
|
22
|
+
font-size: 12.5px;
|
|
23
|
+
}
|
|
24
|
+
pre {
|
|
25
|
+
margin: 0;
|
|
26
|
+
padding: 12px 14px;
|
|
27
|
+
background: #0a0f1a;
|
|
28
|
+
border: 1px solid #1f2a44;
|
|
29
|
+
border-radius: 8px;
|
|
30
|
+
overflow: auto;
|
|
31
|
+
color: #cbd5e1;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/* ───── layout ───── */
|
|
35
|
+
.app { display: grid; grid-template-rows: auto 1fr; min-height: 100vh; }
|
|
36
|
+
header.top {
|
|
37
|
+
padding: 18px 28px;
|
|
38
|
+
border-bottom: 1px solid #1f2a44;
|
|
39
|
+
background: rgba(10,15,26,0.7);
|
|
40
|
+
backdrop-filter: blur(10px);
|
|
41
|
+
display: flex; align-items: center; justify-content: space-between; gap: 16px;
|
|
42
|
+
position: sticky; top: 0; z-index: 50;
|
|
43
|
+
flex-wrap: wrap;
|
|
44
|
+
}
|
|
45
|
+
.brand { display: flex; align-items: center; gap: 10px; }
|
|
46
|
+
.brand .dot {
|
|
47
|
+
width: 10px; height: 10px; border-radius: 999px;
|
|
48
|
+
background: #34d399; box-shadow: 0 0 12px #34d399;
|
|
49
|
+
transition: background 300ms, box-shadow 300ms;
|
|
50
|
+
}
|
|
51
|
+
.brand h1 { margin: 0; font-size: 16px; font-weight: 600; letter-spacing: 0.2px; }
|
|
52
|
+
.brand .sub { font-size: 12px; color: #94a3b8; }
|
|
53
|
+
|
|
54
|
+
.header-right { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
|
|
55
|
+
|
|
56
|
+
nav.tabs { display: flex; gap: 4px; flex-wrap: wrap; }
|
|
57
|
+
nav.tabs button {
|
|
58
|
+
background: transparent; color: #94a3b8;
|
|
59
|
+
border: 1px solid #1f2a44; border-radius: 8px;
|
|
60
|
+
padding: 8px 14px; font: inherit; cursor: pointer;
|
|
61
|
+
transition: 120ms;
|
|
62
|
+
}
|
|
63
|
+
nav.tabs button:hover { color: #e5e7eb; border-color: #334155; }
|
|
64
|
+
nav.tabs button.active {
|
|
65
|
+
color: #e5e7eb; background: #111a2e; border-color: #3b82f6;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.btn-ghost {
|
|
69
|
+
background: transparent; color: #94a3b8;
|
|
70
|
+
border: 1px solid #1f2a44; border-radius: 8px;
|
|
71
|
+
padding: 6px 12px; font: inherit; cursor: pointer; font-size: 12px;
|
|
72
|
+
transition: 120ms;
|
|
73
|
+
}
|
|
74
|
+
.btn-ghost:hover { color: #e5e7eb; border-color: #334155; }
|
|
75
|
+
|
|
76
|
+
main {
|
|
77
|
+
padding: 24px 28px 80px;
|
|
78
|
+
max-width: 1500px; width: 100%; margin: 0 auto;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/* ───── cards ───── */
|
|
82
|
+
.grid {
|
|
83
|
+
display: grid; gap: 16px;
|
|
84
|
+
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
|
85
|
+
}
|
|
86
|
+
.card {
|
|
87
|
+
background: rgba(15,23,42,0.7);
|
|
88
|
+
border: 1px solid #1f2a44;
|
|
89
|
+
border-radius: 12px;
|
|
90
|
+
padding: 18px 20px;
|
|
91
|
+
}
|
|
92
|
+
.card h2 {
|
|
93
|
+
margin: 0 0 14px; font-size: 13px; font-weight: 600;
|
|
94
|
+
color: #94a3b8; text-transform: uppercase; letter-spacing: 0.06em;
|
|
95
|
+
}
|
|
96
|
+
.stat .num {
|
|
97
|
+
font-size: 32px; font-weight: 700; color: #e5e7eb; line-height: 1;
|
|
98
|
+
}
|
|
99
|
+
.stat .label { color: #94a3b8; margin-top: 4px; font-size: 13px; }
|
|
100
|
+
.stat .micro { color: #64748b; margin-top: 6px; font-size: 11.5px; }
|
|
101
|
+
|
|
102
|
+
/* ───── tables ───── */
|
|
103
|
+
.table-wrap { overflow: auto; border-radius: 10px; border: 1px solid #1f2a44; }
|
|
104
|
+
table { width: 100%; border-collapse: collapse; }
|
|
105
|
+
th, td {
|
|
106
|
+
padding: 10px 12px; text-align: left; vertical-align: top;
|
|
107
|
+
border-bottom: 1px solid #1a2238; font-size: 13px;
|
|
108
|
+
}
|
|
109
|
+
th {
|
|
110
|
+
color: #94a3b8; background: #0a1322;
|
|
111
|
+
font-size: 11.5px; text-transform: uppercase; letter-spacing: 0.06em; font-weight: 600;
|
|
112
|
+
}
|
|
113
|
+
tbody tr:hover { background: rgba(59,130,246,0.05); }
|
|
114
|
+
tbody tr:last-child td { border-bottom: 0; }
|
|
115
|
+
|
|
116
|
+
.pill {
|
|
117
|
+
display: inline-block; padding: 2px 8px; border-radius: 999px;
|
|
118
|
+
font-size: 11px; background: #1e293b; color: #cbd5e1;
|
|
119
|
+
border: 1px solid #334155;
|
|
120
|
+
}
|
|
121
|
+
.pill.ok { background: rgba(34,197,94,0.12); color: #86efac; border-color: rgba(34,197,94,0.4); }
|
|
122
|
+
.pill.pending { background: rgba(234,179,8,0.12); color: #fde68a; border-color: rgba(234,179,8,0.4); }
|
|
123
|
+
.pill.rejected { background: rgba(239,68,68,0.12); color: #fca5a5; border-color: rgba(239,68,68,0.4); }
|
|
124
|
+
.pill.applied { background: rgba(168,85,247,0.12); color: #d8b4fe; border-color: rgba(168,85,247,0.4); }
|
|
125
|
+
|
|
126
|
+
/* ───── search ───── */
|
|
127
|
+
.search {
|
|
128
|
+
display: flex; gap: 8px; margin: 0 0 16px;
|
|
129
|
+
}
|
|
130
|
+
.search input, .search select {
|
|
131
|
+
flex: 1; min-width: 0;
|
|
132
|
+
padding: 10px 14px;
|
|
133
|
+
background: #0a1322; color: #e5e7eb;
|
|
134
|
+
border: 1px solid #1f2a44; border-radius: 8px;
|
|
135
|
+
font: inherit; outline: none;
|
|
136
|
+
transition: border-color 120ms, box-shadow 120ms;
|
|
137
|
+
}
|
|
138
|
+
.search input:focus, .search select:focus {
|
|
139
|
+
border-color: #3b82f6;
|
|
140
|
+
box-shadow: 0 0 0 3px rgba(59,130,246,0.15);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/* Custom select: kill native chevron, paint our own */
|
|
144
|
+
.search select {
|
|
145
|
+
appearance: none;
|
|
146
|
+
-webkit-appearance: none;
|
|
147
|
+
-moz-appearance: none;
|
|
148
|
+
flex: 0 0 180px;
|
|
149
|
+
padding-right: 38px;
|
|
150
|
+
cursor: pointer;
|
|
151
|
+
background-image:
|
|
152
|
+
url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 24 24' fill='none' stroke='%2394a3b8' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'><polyline points='6 9 12 15 18 9'/></svg>");
|
|
153
|
+
background-repeat: no-repeat;
|
|
154
|
+
background-position: right 14px center;
|
|
155
|
+
background-size: 14px 14px;
|
|
156
|
+
}
|
|
157
|
+
.search select:hover { border-color: #334155; }
|
|
158
|
+
.search select::-ms-expand { display: none; }
|
|
159
|
+
.search select option {
|
|
160
|
+
background: #0a1322;
|
|
161
|
+
color: #e5e7eb;
|
|
162
|
+
padding: 8px 12px;
|
|
163
|
+
}
|
|
164
|
+
.search select option:checked,
|
|
165
|
+
.search select option:hover {
|
|
166
|
+
background: #1e3a8a;
|
|
167
|
+
color: #ffffff;
|
|
168
|
+
}
|
|
169
|
+
.search button {
|
|
170
|
+
padding: 10px 16px;
|
|
171
|
+
background: #3b82f6; color: white; border: 0; border-radius: 8px;
|
|
172
|
+
cursor: pointer; font: inherit; font-weight: 600;
|
|
173
|
+
}
|
|
174
|
+
.search button:hover { background: #2563eb; }
|
|
175
|
+
|
|
176
|
+
/* ───── copy button ───── */
|
|
177
|
+
.copy-btn {
|
|
178
|
+
float: right; margin-left: 8px; font-size: 11px; padding: 2px 8px;
|
|
179
|
+
background: #1e293b; color: #93c5fd; border: 1px solid #334155;
|
|
180
|
+
border-radius: 6px; cursor: pointer; font-family: inherit; line-height: 1.4;
|
|
181
|
+
vertical-align: middle;
|
|
182
|
+
}
|
|
183
|
+
.copy-btn:hover { background: #1e3a5f; }
|
|
184
|
+
|
|
185
|
+
/* ───── small stuff ───── */
|
|
186
|
+
.row { display: flex; gap: 12px; flex-wrap: wrap; }
|
|
187
|
+
.row > * { flex: 1 1 220px; }
|
|
188
|
+
.muted { color: #94a3b8; }
|
|
189
|
+
.dim { color: #64748b; }
|
|
190
|
+
.strong { color: #e5e7eb; font-weight: 600; }
|
|
191
|
+
|
|
192
|
+
details { border: 1px solid #1f2a44; border-radius: 8px; padding: 8px 12px; background: #0a1322; }
|
|
193
|
+
details + details { margin-top: 8px; }
|
|
194
|
+
details summary { cursor: pointer; padding: 4px 0; }
|
|
195
|
+
details[open] summary { margin-bottom: 8px; color: #93c5fd; }
|
|
196
|
+
|
|
197
|
+
.empty {
|
|
198
|
+
padding: 32px; text-align: center; color: #64748b; border: 1px dashed #1f2a44; border-radius: 12px;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
footer.bot {
|
|
202
|
+
padding: 16px 28px; border-top: 1px solid #1f2a44;
|
|
203
|
+
color: #64748b; font-size: 12px;
|
|
204
|
+
display: flex; justify-content: space-between; gap: 16px; flex-wrap: wrap;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
hr.sep { border: 0; border-top: 1px solid #1f2a44; margin: 24px 0; }
|
|
208
|
+
</style>
|
|
209
|
+
</head>
|
|
210
|
+
<body>
|
|
211
|
+
<div class="app">
|
|
212
|
+
<header class="top">
|
|
213
|
+
<div class="brand">
|
|
214
|
+
<span class="dot" id="liveDot"></span>
|
|
215
|
+
<h1>Kodelyth ECC</h1>
|
|
216
|
+
<span class="sub" id="hostLabel">— dashboard v1.7.0 · localhost</span>
|
|
217
|
+
</div>
|
|
218
|
+
<div class="header-right">
|
|
219
|
+
<span id="connBadge" class="pill ok" title="Real-time SSE connection status">LIVE</span>
|
|
220
|
+
<button id="refreshBtn" class="btn-ghost" title="Refresh active tab (R key)">Refresh</button>
|
|
221
|
+
<nav class="tabs">
|
|
222
|
+
<button data-tab="overview" class="active">Overview</button>
|
|
223
|
+
<button data-tab="memory">Memory</button>
|
|
224
|
+
<button data-tab="evolve">Evolve</button>
|
|
225
|
+
<button data-tab="catalog">Catalog</button>
|
|
226
|
+
<button data-tab="sessions">Sessions</button>
|
|
227
|
+
</nav>
|
|
228
|
+
</div>
|
|
229
|
+
</header>
|
|
230
|
+
|
|
231
|
+
<main>
|
|
232
|
+
<!-- ───────── OVERVIEW ───────── -->
|
|
233
|
+
<section data-panel="overview">
|
|
234
|
+
<div class="grid" id="overviewCards"></div>
|
|
235
|
+
<hr class="sep">
|
|
236
|
+
<div class="row">
|
|
237
|
+
<div class="card">
|
|
238
|
+
<h2>Storage</h2>
|
|
239
|
+
<pre id="storagePre">loading…</pre>
|
|
240
|
+
</div>
|
|
241
|
+
<div class="card">
|
|
242
|
+
<h2>Token budget snapshot</h2>
|
|
243
|
+
<div id="budgetSnapshot">loading…</div>
|
|
244
|
+
</div>
|
|
245
|
+
</div>
|
|
246
|
+
</section>
|
|
247
|
+
|
|
248
|
+
<!-- ───────── MEMORY ───────── -->
|
|
249
|
+
<section data-panel="memory" hidden>
|
|
250
|
+
<div class="grid" id="memoryCards"></div>
|
|
251
|
+
<hr class="sep">
|
|
252
|
+
<div class="card">
|
|
253
|
+
<h2>BM25 search</h2>
|
|
254
|
+
<div class="search">
|
|
255
|
+
<input id="memQuery" placeholder='try "tailwind v4 migration"'>
|
|
256
|
+
<button id="memSearchBtn">Search</button>
|
|
257
|
+
</div>
|
|
258
|
+
<div id="memSearchResults" class="muted">Type a query and hit search.</div>
|
|
259
|
+
</div>
|
|
260
|
+
<hr class="sep">
|
|
261
|
+
<div class="card">
|
|
262
|
+
<h2>Recent memories</h2>
|
|
263
|
+
<div id="recentMemoriesTable">loading…</div>
|
|
264
|
+
</div>
|
|
265
|
+
</section>
|
|
266
|
+
|
|
267
|
+
<!-- ───────── EVOLVE ───────── -->
|
|
268
|
+
<section data-panel="evolve" hidden>
|
|
269
|
+
<div class="grid" id="evolveCards"></div>
|
|
270
|
+
<hr class="sep">
|
|
271
|
+
<div class="row">
|
|
272
|
+
<div class="card">
|
|
273
|
+
<h2>Top reused memories</h2>
|
|
274
|
+
<div id="reuseList">loading…</div>
|
|
275
|
+
</div>
|
|
276
|
+
<div class="card">
|
|
277
|
+
<h2>Top routing-miss clusters</h2>
|
|
278
|
+
<div id="missList">loading…</div>
|
|
279
|
+
</div>
|
|
280
|
+
</div>
|
|
281
|
+
<hr class="sep">
|
|
282
|
+
<div class="card">
|
|
283
|
+
<h2>Proposals</h2>
|
|
284
|
+
<div id="proposalsTable">loading…</div>
|
|
285
|
+
<p class="muted" style="margin-top: 12px; font-size: 12.5px;">
|
|
286
|
+
Run <code>kodelyth-ecc evolve accept <id></code> in your terminal to write a draft to disk.
|
|
287
|
+
The dashboard NEVER auto-applies anything.
|
|
288
|
+
</p>
|
|
289
|
+
</div>
|
|
290
|
+
</section>
|
|
291
|
+
|
|
292
|
+
<!-- ───────── CATALOG ───────── -->
|
|
293
|
+
<section data-panel="catalog" hidden>
|
|
294
|
+
<div class="card">
|
|
295
|
+
<h2>Catalog browser</h2>
|
|
296
|
+
<div class="search">
|
|
297
|
+
<select id="catKind">
|
|
298
|
+
<option value="skills">skills</option>
|
|
299
|
+
<option value="agents">agents</option>
|
|
300
|
+
<option value="commands">commands</option>
|
|
301
|
+
<option value="rules">rules</option>
|
|
302
|
+
<option value="bundles">bundles</option>
|
|
303
|
+
</select>
|
|
304
|
+
<input id="catQuery" placeholder="filter by name, description, tag">
|
|
305
|
+
<button id="catRefreshBtn">Refresh</button>
|
|
306
|
+
</div>
|
|
307
|
+
<div id="catalogResults">loading…</div>
|
|
308
|
+
</div>
|
|
309
|
+
</section>
|
|
310
|
+
|
|
311
|
+
<!-- ───────── SESSIONS ───────── -->
|
|
312
|
+
<section data-panel="sessions" hidden>
|
|
313
|
+
<div class="card">
|
|
314
|
+
<h2>Orchestration / swarm sessions</h2>
|
|
315
|
+
<div id="sessionsList">loading…</div>
|
|
316
|
+
</div>
|
|
317
|
+
</section>
|
|
318
|
+
</main>
|
|
319
|
+
|
|
320
|
+
<footer class="bot">
|
|
321
|
+
<div>
|
|
322
|
+
Localhost only · zero telemetry · all data stays on this machine.
|
|
323
|
+
| Keys <kbd>1</kbd>–<kbd>5</kbd> switch tabs · <kbd>R</kbd> to refresh.
|
|
324
|
+
| Built by <a href="https://github.com/sifxprime" target="_blank" rel="noopener">Shofiqul Islam</a>
|
|
325
|
+
· <a href="https://github.com/sifxprime/kodelyth-ecc" target="_blank" rel="noopener">GitHub</a>
|
|
326
|
+
· <a href="https://www.npmjs.com/package/kodelyth-ecc" target="_blank" rel="noopener">npm</a>
|
|
327
|
+
</div>
|
|
328
|
+
<div id="lastUpdated">last refreshed: —</div>
|
|
329
|
+
</footer>
|
|
330
|
+
</div>
|
|
331
|
+
|
|
332
|
+
<script>
|
|
333
|
+
// ───── tiny helpers ─────
|
|
334
|
+
const $ = sel => document.querySelector(sel);
|
|
335
|
+
const $$ = sel => Array.from(document.querySelectorAll(sel));
|
|
336
|
+
const fmtDate = s => s ? new Date(s).toLocaleString() : '—';
|
|
337
|
+
const escapeHtml = s => String(s == null ? '' : s)
|
|
338
|
+
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
339
|
+
.replace(/"/g, '"').replace(/'/g, ''');
|
|
340
|
+
|
|
341
|
+
async function api(path) {
|
|
342
|
+
const r = await fetch(path, { headers: { Accept: 'application/json' } });
|
|
343
|
+
if (!r.ok) throw new Error(`${path} → ${r.status}`);
|
|
344
|
+
return r.json();
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function setLastUpdated() {
|
|
348
|
+
$('#lastUpdated').textContent = `last refreshed: ${new Date().toLocaleTimeString()}`;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// ───── tab state + switching ─────
|
|
352
|
+
let currentTab = 'overview';
|
|
353
|
+
|
|
354
|
+
function switchTab(name) {
|
|
355
|
+
$$('.tabs button').forEach(x => x.classList.toggle('active', x.dataset.tab === name));
|
|
356
|
+
$$('section[data-panel]').forEach(s => s.hidden = s.dataset.panel !== name);
|
|
357
|
+
currentTab = name;
|
|
358
|
+
onTabShown(name);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
$$('.tabs button').forEach(b => b.addEventListener('click', () => switchTab(b.dataset.tab)));
|
|
362
|
+
|
|
363
|
+
// ───── overview ─────
|
|
364
|
+
function statCard(num, label, micro = '') {
|
|
365
|
+
return `<div class="card stat">
|
|
366
|
+
<div class="num">${num}</div>
|
|
367
|
+
<div class="label">${escapeHtml(label)}</div>
|
|
368
|
+
${micro ? `<div class="micro">${escapeHtml(micro)}</div>` : ''}
|
|
369
|
+
</div>`;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
async function loadOverview() {
|
|
373
|
+
try {
|
|
374
|
+
const o = await api('/api/overview');
|
|
375
|
+
$('#hostLabel').textContent = `— dashboard v1.7.0 · ${location.host} · ${o.package_root.split('/').slice(-2).join('/')}`;
|
|
376
|
+
$('#overviewCards').innerHTML = [
|
|
377
|
+
statCard(o.catalog.agents, 'Agents'),
|
|
378
|
+
statCard(o.catalog.skills, 'Skills'),
|
|
379
|
+
statCard(o.catalog.commands, 'Commands'),
|
|
380
|
+
statCard(o.catalog.rules, 'Rules'),
|
|
381
|
+
statCard(o.catalog.bundles, 'Bundles'),
|
|
382
|
+
statCard(o.memory.total, 'Memories captured', `${o.memory.projects} projects`),
|
|
383
|
+
statCard(o.evolve.surfaces_total, 'Memory surfaces', 'cumulative across sessions'),
|
|
384
|
+
statCard(o.evolve.routing_misses, 'Routing misses', 'substantive prompts w/ no recall'),
|
|
385
|
+
statCard(o.evolve.proposals.pending, 'Pending proposals', `${o.evolve.proposals.total} total tracked`),
|
|
386
|
+
statCard(o.swarm.sessions, 'Swarm sessions', 'in this repo'),
|
|
387
|
+
].join('');
|
|
388
|
+
$('#storagePre').textContent = JSON.stringify(o.storage, null, 2);
|
|
389
|
+
|
|
390
|
+
const tb = await api('/api/token-budget');
|
|
391
|
+
if (!tb.sessions.length) {
|
|
392
|
+
$('#budgetSnapshot').innerHTML = `<div class="empty">No token-budget hook activity recorded yet.</div>`;
|
|
393
|
+
} else {
|
|
394
|
+
$('#budgetSnapshot').innerHTML = `
|
|
395
|
+
<div class="stat" style="display:flex;gap:12px;align-items:baseline;">
|
|
396
|
+
<span class="num">${tb.total_tokens.toLocaleString()}</span>
|
|
397
|
+
<span class="label">tokens across ${tb.sessions.length} sessions</span>
|
|
398
|
+
</div>
|
|
399
|
+
<div class="muted" style="margin-top:8px;font-size:12px;">
|
|
400
|
+
most-recent: ${escapeHtml(tb.sessions[0]?.session_id || '—')} · last seen ${fmtDate(tb.sessions[0]?.last_event)}
|
|
401
|
+
</div>`;
|
|
402
|
+
}
|
|
403
|
+
setLastUpdated();
|
|
404
|
+
} catch (e) {
|
|
405
|
+
$('#overviewCards').innerHTML = `<div class="empty">failed to load overview: ${escapeHtml(e.message)}</div>`;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// ───── memory ─────
|
|
410
|
+
async function loadMemory() {
|
|
411
|
+
try {
|
|
412
|
+
const m = await api('/api/memory');
|
|
413
|
+
$('#memoryCards').innerHTML = [
|
|
414
|
+
statCard(m.stats.total || 0, 'Total memories'),
|
|
415
|
+
statCard(m.stats.projects || 0, 'Projects'),
|
|
416
|
+
statCard(Object.keys(m.stats.byLanguage || {}).length, 'Languages'),
|
|
417
|
+
statCard((m.stats.topTags || []).length, 'Tag classes'),
|
|
418
|
+
].join('');
|
|
419
|
+
|
|
420
|
+
if (!m.recent.length) {
|
|
421
|
+
$('#recentMemoriesTable').innerHTML = `<div class="empty">No captures yet. Use <code>/memory remember</code> or the MCP <code>capture_memory</code> tool.</div>`;
|
|
422
|
+
} else {
|
|
423
|
+
$('#recentMemoriesTable').innerHTML = `<div class="table-wrap"><table>
|
|
424
|
+
<thead><tr><th>Captured</th><th>Problem</th><th>Tags</th><th>Lang</th><th>Source</th></tr></thead>
|
|
425
|
+
<tbody>${m.recent.map(r => `<tr>
|
|
426
|
+
<td class="muted">${fmtDate(r.captured_at)}</td>
|
|
427
|
+
<td><div class="strong">${escapeHtml(r.problem)}</div><div class="dim" style="font-size:12px;margin-top:4px;">${escapeHtml(r.approach.slice(0,180))}${r.approach.length > 180 ? '…' : ''}</div></td>
|
|
428
|
+
<td>${(r.tags || []).map(t => `<span class="pill">${escapeHtml(t)}</span>`).join(' ')}</td>
|
|
429
|
+
<td>${escapeHtml(r.language || '—')}</td>
|
|
430
|
+
<td class="muted">${escapeHtml(r.source || '—')}</td>
|
|
431
|
+
</tr>`).join('')}</tbody></table></div>`;
|
|
432
|
+
}
|
|
433
|
+
setLastUpdated();
|
|
434
|
+
} catch (e) {
|
|
435
|
+
$('#memoryCards').innerHTML = `<div class="empty">failed to load memory: ${escapeHtml(e.message)}</div>`;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
let _searchInFlight = false;
|
|
440
|
+
async function runMemorySearch() {
|
|
441
|
+
if (_searchInFlight) return; // prevent key-repeat burst
|
|
442
|
+
const q = $('#memQuery').value.trim();
|
|
443
|
+
if (!q) return;
|
|
444
|
+
_searchInFlight = true;
|
|
445
|
+
$('#memSearchResults').textContent = `searching for "${q}"…`;
|
|
446
|
+
try {
|
|
447
|
+
const r = await api('/api/memory/search?q=' + encodeURIComponent(q));
|
|
448
|
+
if (!r.results.length) {
|
|
449
|
+
$('#memSearchResults').innerHTML = `<div class="empty">No matches.</div>`;
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
$('#memSearchResults').innerHTML = r.results.map(m => `
|
|
453
|
+
<details>
|
|
454
|
+
<summary>
|
|
455
|
+
<span class="pill">score ${m.score}</span> ${escapeHtml(m.problem)}
|
|
456
|
+
<button class="copy-btn" data-copy="${escapeHtml(JSON.stringify({ problem: m.problem, approach: m.approach, tags: m.tags }))}" onclick="event.stopPropagation();navigator.clipboard?.writeText(this.dataset.copy).then(()=>{this.textContent='copied';setTimeout(()=>this.textContent='copy',1500)})">copy</button>
|
|
457
|
+
</summary>
|
|
458
|
+
<pre>${escapeHtml(m.approach)}</pre>
|
|
459
|
+
<div class="muted" style="margin-top:6px;font-size:12px;">${(m.tags||[]).map(t=>`<span class="pill">${escapeHtml(t)}</span>`).join(' ')} · ${escapeHtml(m.language || '—')}</div>
|
|
460
|
+
</details>`).join('');
|
|
461
|
+
} catch (e) {
|
|
462
|
+
$('#memSearchResults').innerHTML = `<div class="empty">search failed: ${escapeHtml(e.message)}</div>`;
|
|
463
|
+
} finally {
|
|
464
|
+
_searchInFlight = false;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// ───── evolve ─────
|
|
469
|
+
async function loadEvolve() {
|
|
470
|
+
try {
|
|
471
|
+
const e = await api('/api/evolve');
|
|
472
|
+
$('#evolveCards').innerHTML = [
|
|
473
|
+
statCard(e.reuse.total_memories_tracked, 'Memories tracked'),
|
|
474
|
+
statCard(e.reuse.total_surfaces, 'Surfaces'),
|
|
475
|
+
statCard(e.miss.total_misses, 'Routing misses'),
|
|
476
|
+
statCard(e.miss.unique_prompts, 'Unique miss prompts'),
|
|
477
|
+
statCard(e.proposals.length, 'Proposals'),
|
|
478
|
+
].join('');
|
|
479
|
+
|
|
480
|
+
$('#reuseList').innerHTML = e.reuse.entries.length
|
|
481
|
+
? e.reuse.entries.slice(0, 10).map(r => `<div style="margin-bottom:10px;">
|
|
482
|
+
<div class="strong">${escapeHtml(r.id)}</div>
|
|
483
|
+
<div class="muted" style="font-size:12px;">${r.count} surfaces · ${(r.sessions||[]).length} sessions · last ${fmtDate(r.lastSurfaced)}</div>
|
|
484
|
+
</div>`).join('')
|
|
485
|
+
: `<div class="empty">No reuse signals yet.</div>`;
|
|
486
|
+
|
|
487
|
+
$('#missList').innerHTML = e.miss.entries.length
|
|
488
|
+
? e.miss.entries.slice(0, 10).map(m => `<div style="margin-bottom:10px;">
|
|
489
|
+
<div class="strong">${(m.tokens || []).slice(0,4).map(t=>`<span class="pill">${escapeHtml(t)}</span>`).join(' ')}</div>
|
|
490
|
+
<div class="muted" style="font-size:12px;margin-top:4px;">${m.count} occurrence${m.count===1?'':'s'} · last ${fmtDate(m.lastSeen)}</div>
|
|
491
|
+
${(m.samples || [])[0] ? `<div class="dim" style="font-size:12px;margin-top:4px;">"${escapeHtml(m.samples[0])}"</div>` : ''}
|
|
492
|
+
</div>`).join('')
|
|
493
|
+
: `<div class="empty">No routing-miss clusters yet.</div>`;
|
|
494
|
+
|
|
495
|
+
$('#proposalsTable').innerHTML = e.proposals.length
|
|
496
|
+
? `<div class="table-wrap"><table>
|
|
497
|
+
<thead><tr><th>ID</th><th>Type</th><th>Status</th><th>Target</th><th>Why</th></tr></thead>
|
|
498
|
+
<tbody>${e.proposals.map(p => `<tr>
|
|
499
|
+
<td><code>${escapeHtml(p.id)}</code></td>
|
|
500
|
+
<td>${escapeHtml(p.type)}</td>
|
|
501
|
+
<td><span class="pill ${escapeHtml(p.status||'')}">${escapeHtml(p.status||'?')}</span></td>
|
|
502
|
+
<td class="muted">${escapeHtml(p.target_path || '—')}</td>
|
|
503
|
+
<td>${escapeHtml(p.rationale || '—')}</td>
|
|
504
|
+
</tr>`).join('')}</tbody></table></div>`
|
|
505
|
+
: `<div class="empty">No proposals yet. Run <code>kodelyth-ecc evolve analyze</code>.</div>`;
|
|
506
|
+
setLastUpdated();
|
|
507
|
+
} catch (err) {
|
|
508
|
+
$('#evolveCards').innerHTML = `<div class="empty">failed to load evolve: ${escapeHtml(err.message)}</div>`;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// ───── catalog ─────
|
|
513
|
+
async function loadCatalog() {
|
|
514
|
+
const kind = $('#catKind').value;
|
|
515
|
+
const q = $('#catQuery').value.trim();
|
|
516
|
+
$('#catalogResults').textContent = 'loading…';
|
|
517
|
+
try {
|
|
518
|
+
const r = await api(`/api/catalog?kind=${encodeURIComponent(kind)}&q=${encodeURIComponent(q)}&limit=200`);
|
|
519
|
+
if (!r.items.length) {
|
|
520
|
+
$('#catalogResults').innerHTML = `<div class="empty">no matches</div>`;
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
$('#catalogResults').innerHTML = `<div class="muted" style="margin:8px 0 12px;">showing ${r.items.length} of ${r.counts[kind] || 0} ${kind}</div>
|
|
524
|
+
<div class="table-wrap"><table>
|
|
525
|
+
<thead><tr><th>Name</th><th>Description</th><th>Tags</th></tr></thead>
|
|
526
|
+
<tbody>${r.items.map(it => `<tr>
|
|
527
|
+
<td><span class="strong">${escapeHtml(it.name || '')}</span></td>
|
|
528
|
+
<td>${escapeHtml(it.description || '')}</td>
|
|
529
|
+
<td>${(it.tags || []).slice(0, 6).map(t => `<span class="pill">${escapeHtml(t)}</span>`).join(' ')}</td>
|
|
530
|
+
</tr>`).join('')}</tbody></table></div>`;
|
|
531
|
+
setLastUpdated();
|
|
532
|
+
} catch (e) {
|
|
533
|
+
$('#catalogResults').innerHTML = `<div class="empty">failed: ${escapeHtml(e.message)}</div>`;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// ───── sessions ─────
|
|
538
|
+
async function loadSessions() {
|
|
539
|
+
try {
|
|
540
|
+
const r = await api('/api/sessions');
|
|
541
|
+
if (!r.sessions.length) {
|
|
542
|
+
$('#sessionsList').innerHTML = `<div class="empty">No swarm sessions in <code>.orchestration/</code>. Try <code>kodelyth-ecc swarm --task "…"</code>.</div>`;
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
$('#sessionsList').innerHTML = r.sessions.map(s => `<details>
|
|
546
|
+
<summary><span class="strong">${escapeHtml(s.session)}</span> <span class="muted">· ${s.worker_count} workers · ${fmtDate(s.modified)}</span></summary>
|
|
547
|
+
<div class="muted" style="font-size:12px;margin-bottom:10px;">${escapeHtml(s.path)}</div>
|
|
548
|
+
<div>${s.workers.map(w => `<span class="pill" style="margin-right:6px;">${escapeHtml(w)}</span>`).join('')}</div>
|
|
549
|
+
<button data-session="${escapeHtml(s.session)}" class="loadDetail" style="margin-top:10px;background:#1f2937;color:#cbd5e1;border:1px solid #334155;padding:6px 12px;border-radius:6px;cursor:pointer;font-size:12px;">load detail</button>
|
|
550
|
+
<div class="detail-slot" style="margin-top:10px;"></div>
|
|
551
|
+
</details>`).join('');
|
|
552
|
+
|
|
553
|
+
$$('button.loadDetail').forEach(btn => btn.addEventListener('click', async () => {
|
|
554
|
+
const slot = btn.parentElement.querySelector('.detail-slot');
|
|
555
|
+
slot.textContent = 'loading…';
|
|
556
|
+
try {
|
|
557
|
+
const d = await api('/api/sessions/' + encodeURIComponent(btn.dataset.session));
|
|
558
|
+
slot.innerHTML = d.workers.map(w => `<details><summary><code>${escapeHtml(w.slug)}</code> · task=${w.has_task} handoff=${w.has_handoff} status=${w.has_status}</summary>
|
|
559
|
+
${w.task_excerpt ? `<h4 class="muted" style="margin:10px 0 4px;">task.md</h4><pre>${escapeHtml(w.task_excerpt.slice(0,800))}</pre>` : ''}
|
|
560
|
+
${w.handoff_excerpt ? `<h4 class="muted" style="margin:10px 0 4px;">handoff.md</h4><pre>${escapeHtml(w.handoff_excerpt.slice(0,800))}</pre>` : ''}
|
|
561
|
+
${w.status_excerpt ? `<h4 class="muted" style="margin:10px 0 4px;">status.md</h4><pre>${escapeHtml(w.status_excerpt.slice(0,800))}</pre>` : ''}
|
|
562
|
+
</details>`).join('');
|
|
563
|
+
} catch (e) {
|
|
564
|
+
slot.innerHTML = `<div class="empty">load failed: ${escapeHtml(e.message)}</div>`;
|
|
565
|
+
}
|
|
566
|
+
}));
|
|
567
|
+
setLastUpdated();
|
|
568
|
+
} catch (e) {
|
|
569
|
+
$('#sessionsList').innerHTML = `<div class="empty">failed: ${escapeHtml(e.message)}</div>`;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// ───── tab dispatcher ─────
|
|
574
|
+
function onTabShown(tab) {
|
|
575
|
+
if (tab === 'overview') loadOverview();
|
|
576
|
+
if (tab === 'memory') loadMemory();
|
|
577
|
+
if (tab === 'evolve') loadEvolve();
|
|
578
|
+
if (tab === 'catalog') loadCatalog();
|
|
579
|
+
if (tab === 'sessions') loadSessions();
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// ───── real-time: SSE connection ─────
|
|
583
|
+
function updateConnBadge(online) {
|
|
584
|
+
const badge = $('#connBadge');
|
|
585
|
+
const dot = $('#liveDot');
|
|
586
|
+
if (online) {
|
|
587
|
+
badge.textContent = 'LIVE';
|
|
588
|
+
badge.className = 'pill ok';
|
|
589
|
+
dot.style.background = '#34d399';
|
|
590
|
+
dot.style.boxShadow = '0 0 12px #34d399';
|
|
591
|
+
} else {
|
|
592
|
+
badge.textContent = 'offline';
|
|
593
|
+
badge.className = 'pill rejected';
|
|
594
|
+
dot.style.background = '#ef4444';
|
|
595
|
+
dot.style.boxShadow = 'none';
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
let _es = null;
|
|
600
|
+
function connectSSE() {
|
|
601
|
+
if (_es) { try { _es.close(); } catch {} }
|
|
602
|
+
_es = new EventSource('/api/events');
|
|
603
|
+
_es.onopen = () => updateConnBadge(true);
|
|
604
|
+
_es.onmessage = evt => {
|
|
605
|
+
try {
|
|
606
|
+
const msg = JSON.parse(evt.data);
|
|
607
|
+
if (msg.type === 'connected') { updateConnBadge(true); return; }
|
|
608
|
+
// Only refresh on actual data changes. Heartbeat is a keepalive only —
|
|
609
|
+
// triggering a full reload on every heartbeat wastes 6 API calls/minute.
|
|
610
|
+
if (msg.type === 'data-changed') onTabShown(currentTab);
|
|
611
|
+
} catch { /* malformed event — ignore */ }
|
|
612
|
+
};
|
|
613
|
+
_es.onerror = () => {
|
|
614
|
+
updateConnBadge(false);
|
|
615
|
+
// EventSource auto-reconnects with back-off — no manual retry needed.
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// ───── keyboard shortcuts ─────
|
|
620
|
+
// 1–5: switch to that tab | R: refresh active tab
|
|
621
|
+
document.addEventListener('keydown', e => {
|
|
622
|
+
if (['INPUT', 'SELECT', 'TEXTAREA'].includes(e.target.tagName)) return;
|
|
623
|
+
if (e.metaKey || e.ctrlKey || e.altKey) return;
|
|
624
|
+
const TAB_KEYS = { '1': 'overview', '2': 'memory', '3': 'evolve', '4': 'catalog', '5': 'sessions' };
|
|
625
|
+
if (TAB_KEYS[e.key]) { switchTab(TAB_KEYS[e.key]); return; }
|
|
626
|
+
if (e.key === 'r' || e.key === 'R') onTabShown(currentTab);
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
// ───── boot ─────
|
|
630
|
+
$('#memSearchBtn').addEventListener('click', runMemorySearch);
|
|
631
|
+
$('#memQuery').addEventListener('keydown', e => { if (e.key === 'Enter') runMemorySearch(); });
|
|
632
|
+
$('#catRefreshBtn').addEventListener('click', loadCatalog);
|
|
633
|
+
let _catKindTimer = null;
|
|
634
|
+
$('#catKind').addEventListener('change', () => {
|
|
635
|
+
clearTimeout(_catKindTimer);
|
|
636
|
+
_catKindTimer = setTimeout(loadCatalog, 100);
|
|
637
|
+
});
|
|
638
|
+
$('#catQuery').addEventListener('keydown', e => { if (e.key === 'Enter') loadCatalog(); });
|
|
639
|
+
$('#refreshBtn').addEventListener('click', () => onTabShown(currentTab));
|
|
640
|
+
|
|
641
|
+
loadOverview();
|
|
642
|
+
connectSSE();
|
|
643
|
+
</script>
|
|
644
|
+
</body>
|
|
645
|
+
</html>
|