kodelyth-ecc 1.2.2 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +101 -181
- package/CHANGELOG.md +32 -0
- package/CLAUDE.md +72 -63
- package/KODELYTH.md +79 -44
- package/README.md +189 -204
- package/VERSION +1 -1
- package/agents/dependency-doctor.md +120 -0
- package/agents/env-debugger.md +154 -0
- package/agents/flake-hunter.md +142 -0
- package/agents/git-rescue.md +133 -0
- package/agents/release-captain.md +190 -0
- package/install.ps1 +9 -9
- package/install.sh +11 -97
- package/package.json +2 -2
- package/rules/common/agent-intent-routing.md +337 -0
- package/skills/agent-handoff/SKILL.md +184 -0
- package/skills/intent-routing/SKILL.md +134 -0
- package/dashboard/lib/agent-tracker.js +0 -366
- package/dashboard/lib/aggregator.js +0 -119
- package/dashboard/lib/cost-calculator.js +0 -50
- package/dashboard/lib/platform-detector.js +0 -89
- package/dashboard/lib/readers/antigravity-reader.js +0 -113
- package/dashboard/lib/readers/claude-reader.js +0 -135
- package/dashboard/lib/readers/codex-reader.js +0 -192
- package/dashboard/lib/readers/cursor-reader.js +0 -135
- package/dashboard/lib/readers/opencode-reader.js +0 -201
- package/dashboard/lib/readers/windsurf-reader.js +0 -146
- package/dashboard/package.json +0 -24
- package/dashboard/public/index.html +0 -1221
- package/dashboard/server.js +0 -119
- package/scripts/agent-tracker-hook.js +0 -81
- package/social/readme-lens.svg +0 -140
- package/social/readme-savings.svg +0 -56
|
@@ -1,1221 +0,0 @@
|
|
|
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.0"/>
|
|
6
|
-
<title>Kodelyth Lens — AI Agent Dashboard</title>
|
|
7
|
-
<style>
|
|
8
|
-
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
9
|
-
|
|
10
|
-
:root {
|
|
11
|
-
--bg: #f1f5f9;
|
|
12
|
-
--surface: #ffffff;
|
|
13
|
-
--surface2: #f8fafc;
|
|
14
|
-
--border: #e2e8f0;
|
|
15
|
-
--border2: #cbd5e1;
|
|
16
|
-
--text: #0f172a;
|
|
17
|
-
--text2: #334155;
|
|
18
|
-
--muted: #64748b;
|
|
19
|
-
--subtle: #94a3b8;
|
|
20
|
-
|
|
21
|
-
--purple: #7c3aed;
|
|
22
|
-
--purple-l: #ede9fe;
|
|
23
|
-
--green: #059669;
|
|
24
|
-
--green-l: #d1fae5;
|
|
25
|
-
--cyan: #0891b2;
|
|
26
|
-
--cyan-l: #cffafe;
|
|
27
|
-
--red: #dc2626;
|
|
28
|
-
--red-l: #fee2e2;
|
|
29
|
-
--amber: #d97706;
|
|
30
|
-
--amber-l: #fef3c7;
|
|
31
|
-
--blue: #2563eb;
|
|
32
|
-
--blue-l: #dbeafe;
|
|
33
|
-
--rose: #e11d48;
|
|
34
|
-
--rose-l: #ffe4e6;
|
|
35
|
-
--indigo: #4f46e5;
|
|
36
|
-
--indigo-l: #e0e7ff;
|
|
37
|
-
|
|
38
|
-
--shadow-sm: 0 1px 3px rgba(0,0,0,.06), 0 1px 2px rgba(0,0,0,.04);
|
|
39
|
-
--shadow: 0 4px 6px -1px rgba(0,0,0,.07), 0 2px 4px -1px rgba(0,0,0,.04);
|
|
40
|
-
--shadow-md: 0 10px 15px -3px rgba(0,0,0,.08), 0 4px 6px -2px rgba(0,0,0,.04);
|
|
41
|
-
--radius: 12px;
|
|
42
|
-
--radius-sm: 8px;
|
|
43
|
-
--radius-lg: 16px;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
body {
|
|
47
|
-
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Inter', sans-serif;
|
|
48
|
-
background: var(--bg);
|
|
49
|
-
color: var(--text);
|
|
50
|
-
font-size: 14px;
|
|
51
|
-
line-height: 1.5;
|
|
52
|
-
min-height: 100vh;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/* ── Header ─────────────────────────────────────────────────────────────── */
|
|
56
|
-
.header {
|
|
57
|
-
background: var(--surface);
|
|
58
|
-
border-bottom: 1px solid var(--border);
|
|
59
|
-
padding: 0 24px;
|
|
60
|
-
height: 60px;
|
|
61
|
-
display: flex;
|
|
62
|
-
align-items: center;
|
|
63
|
-
gap: 16px;
|
|
64
|
-
position: sticky;
|
|
65
|
-
top: 0;
|
|
66
|
-
z-index: 100;
|
|
67
|
-
box-shadow: var(--shadow-sm);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
.logo {
|
|
71
|
-
display: flex;
|
|
72
|
-
align-items: center;
|
|
73
|
-
gap: 10px;
|
|
74
|
-
text-decoration: none;
|
|
75
|
-
color: inherit;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
.logo-icon {
|
|
79
|
-
width: 32px;
|
|
80
|
-
height: 32px;
|
|
81
|
-
background: linear-gradient(135deg, #7c3aed, #2563eb);
|
|
82
|
-
border-radius: 8px;
|
|
83
|
-
display: flex;
|
|
84
|
-
align-items: center;
|
|
85
|
-
justify-content: center;
|
|
86
|
-
color: #fff;
|
|
87
|
-
font-weight: 800;
|
|
88
|
-
font-size: 13px;
|
|
89
|
-
letter-spacing: -0.5px;
|
|
90
|
-
flex-shrink: 0;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
.logo-name { font-weight: 700; font-size: 15px; color: var(--text); }
|
|
94
|
-
.logo-badge {
|
|
95
|
-
font-size: 11px;
|
|
96
|
-
font-weight: 600;
|
|
97
|
-
background: var(--indigo-l);
|
|
98
|
-
color: var(--indigo);
|
|
99
|
-
padding: 2px 8px;
|
|
100
|
-
border-radius: 20px;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
.header-spacer { flex: 1; }
|
|
104
|
-
|
|
105
|
-
.updated-at {
|
|
106
|
-
font-size: 12px;
|
|
107
|
-
color: var(--muted);
|
|
108
|
-
display: flex;
|
|
109
|
-
align-items: center;
|
|
110
|
-
gap: 6px;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
.live-dot {
|
|
114
|
-
width: 7px;
|
|
115
|
-
height: 7px;
|
|
116
|
-
border-radius: 50%;
|
|
117
|
-
background: var(--green);
|
|
118
|
-
animation: pulse 2s ease-in-out infinite;
|
|
119
|
-
}
|
|
120
|
-
@keyframes pulse {
|
|
121
|
-
0%, 100% { opacity: 1; transform: scale(1); }
|
|
122
|
-
50% { opacity: .5; transform: scale(.8); }
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
.btn-refresh {
|
|
126
|
-
display: flex;
|
|
127
|
-
align-items: center;
|
|
128
|
-
gap: 6px;
|
|
129
|
-
padding: 7px 14px;
|
|
130
|
-
border: 1px solid var(--border2);
|
|
131
|
-
border-radius: var(--radius-sm);
|
|
132
|
-
background: var(--surface);
|
|
133
|
-
color: var(--text2);
|
|
134
|
-
font-size: 13px;
|
|
135
|
-
font-weight: 500;
|
|
136
|
-
cursor: pointer;
|
|
137
|
-
transition: all .15s;
|
|
138
|
-
}
|
|
139
|
-
.btn-refresh:hover { background: var(--surface2); border-color: var(--purple); color: var(--purple); }
|
|
140
|
-
.btn-refresh.spinning svg { animation: spin .7s linear infinite; }
|
|
141
|
-
@keyframes spin { to { transform: rotate(360deg); } }
|
|
142
|
-
|
|
143
|
-
/* ── Main layout ────────────────────────────────────────────────────────── */
|
|
144
|
-
.main {
|
|
145
|
-
max-width: 1400px;
|
|
146
|
-
margin: 0 auto;
|
|
147
|
-
padding: 24px;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
.page-title {
|
|
151
|
-
font-size: 22px;
|
|
152
|
-
font-weight: 700;
|
|
153
|
-
color: var(--text);
|
|
154
|
-
margin-bottom: 4px;
|
|
155
|
-
}
|
|
156
|
-
.page-sub {
|
|
157
|
-
font-size: 13px;
|
|
158
|
-
color: var(--muted);
|
|
159
|
-
margin-bottom: 24px;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
/* ── Stat cards ─────────────────────────────────────────────────────────── */
|
|
163
|
-
.stats-grid {
|
|
164
|
-
display: grid;
|
|
165
|
-
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
|
166
|
-
gap: 16px;
|
|
167
|
-
margin-bottom: 24px;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
.stat-card {
|
|
171
|
-
background: var(--surface);
|
|
172
|
-
border: 1px solid var(--border);
|
|
173
|
-
border-radius: var(--radius);
|
|
174
|
-
padding: 20px;
|
|
175
|
-
box-shadow: var(--shadow-sm);
|
|
176
|
-
transition: box-shadow .2s, transform .2s;
|
|
177
|
-
}
|
|
178
|
-
.stat-card:hover { box-shadow: var(--shadow); transform: translateY(-1px); }
|
|
179
|
-
|
|
180
|
-
.stat-label {
|
|
181
|
-
font-size: 12px;
|
|
182
|
-
font-weight: 600;
|
|
183
|
-
text-transform: uppercase;
|
|
184
|
-
letter-spacing: .6px;
|
|
185
|
-
color: var(--muted);
|
|
186
|
-
margin-bottom: 12px;
|
|
187
|
-
display: flex;
|
|
188
|
-
align-items: center;
|
|
189
|
-
gap: 6px;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
.stat-icon {
|
|
193
|
-
width: 28px;
|
|
194
|
-
height: 28px;
|
|
195
|
-
border-radius: 7px;
|
|
196
|
-
display: flex;
|
|
197
|
-
align-items: center;
|
|
198
|
-
justify-content: center;
|
|
199
|
-
font-size: 14px;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
.stat-value {
|
|
203
|
-
font-size: 28px;
|
|
204
|
-
font-weight: 800;
|
|
205
|
-
color: var(--text);
|
|
206
|
-
line-height: 1;
|
|
207
|
-
margin-bottom: 6px;
|
|
208
|
-
font-variant-numeric: tabular-nums;
|
|
209
|
-
letter-spacing: -1px;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
.stat-sub {
|
|
213
|
-
font-size: 12px;
|
|
214
|
-
color: var(--muted);
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
.stat-delta {
|
|
218
|
-
font-size: 12px;
|
|
219
|
-
font-weight: 600;
|
|
220
|
-
padding: 2px 7px;
|
|
221
|
-
border-radius: 20px;
|
|
222
|
-
display: inline-block;
|
|
223
|
-
margin-top: 6px;
|
|
224
|
-
}
|
|
225
|
-
.delta-up { background: var(--green-l); color: var(--green); }
|
|
226
|
-
.delta-down { background: var(--red-l); color: var(--red); }
|
|
227
|
-
.delta-neu { background: var(--surface2); color: var(--muted); }
|
|
228
|
-
|
|
229
|
-
/* ── Two-column grid ────────────────────────────────────────────────────── */
|
|
230
|
-
.two-col {
|
|
231
|
-
display: grid;
|
|
232
|
-
grid-template-columns: 1fr 1fr;
|
|
233
|
-
gap: 20px;
|
|
234
|
-
margin-bottom: 20px;
|
|
235
|
-
}
|
|
236
|
-
@media (max-width: 900px) { .two-col { grid-template-columns: 1fr; } }
|
|
237
|
-
|
|
238
|
-
.three-col {
|
|
239
|
-
display: grid;
|
|
240
|
-
grid-template-columns: 2fr 1fr;
|
|
241
|
-
gap: 20px;
|
|
242
|
-
margin-bottom: 20px;
|
|
243
|
-
}
|
|
244
|
-
@media (max-width: 1100px) { .three-col { grid-template-columns: 1fr; } }
|
|
245
|
-
|
|
246
|
-
/* ── Card ───────────────────────────────────────────────────────────────── */
|
|
247
|
-
.card {
|
|
248
|
-
background: var(--surface);
|
|
249
|
-
border: 1px solid var(--border);
|
|
250
|
-
border-radius: var(--radius);
|
|
251
|
-
box-shadow: var(--shadow-sm);
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
.card-header {
|
|
255
|
-
display: flex;
|
|
256
|
-
align-items: center;
|
|
257
|
-
justify-content: space-between;
|
|
258
|
-
padding: 18px 20px 0;
|
|
259
|
-
margin-bottom: 16px;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
.card-title {
|
|
263
|
-
font-size: 15px;
|
|
264
|
-
font-weight: 700;
|
|
265
|
-
color: var(--text);
|
|
266
|
-
display: flex;
|
|
267
|
-
align-items: center;
|
|
268
|
-
gap: 8px;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
.card-title-icon {
|
|
272
|
-
width: 26px;
|
|
273
|
-
height: 26px;
|
|
274
|
-
border-radius: 7px;
|
|
275
|
-
display: flex;
|
|
276
|
-
align-items: center;
|
|
277
|
-
justify-content: center;
|
|
278
|
-
font-size: 13px;
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
.card-sub {
|
|
282
|
-
font-size: 12px;
|
|
283
|
-
color: var(--muted);
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
.card-body { padding: 0 20px 20px; }
|
|
287
|
-
|
|
288
|
-
/* ── Agent leaderboard ──────────────────────────────────────────────────── */
|
|
289
|
-
.agent-list { display: flex; flex-direction: column; gap: 8px; }
|
|
290
|
-
|
|
291
|
-
.agent-row {
|
|
292
|
-
display: grid;
|
|
293
|
-
grid-template-columns: 24px 1fr auto;
|
|
294
|
-
align-items: center;
|
|
295
|
-
gap: 10px;
|
|
296
|
-
padding: 10px 12px;
|
|
297
|
-
border-radius: var(--radius-sm);
|
|
298
|
-
background: var(--surface2);
|
|
299
|
-
border: 1px solid transparent;
|
|
300
|
-
transition: all .15s;
|
|
301
|
-
cursor: default;
|
|
302
|
-
}
|
|
303
|
-
.agent-row:hover { border-color: var(--border2); background: #f8f9ff; }
|
|
304
|
-
|
|
305
|
-
.agent-rank {
|
|
306
|
-
font-size: 11px;
|
|
307
|
-
font-weight: 700;
|
|
308
|
-
color: var(--muted);
|
|
309
|
-
text-align: center;
|
|
310
|
-
width: 24px;
|
|
311
|
-
}
|
|
312
|
-
.agent-rank.top { color: var(--purple); }
|
|
313
|
-
|
|
314
|
-
.agent-info { min-width: 0; }
|
|
315
|
-
.agent-name {
|
|
316
|
-
font-size: 13px;
|
|
317
|
-
font-weight: 600;
|
|
318
|
-
color: var(--text);
|
|
319
|
-
font-family: 'SF Mono', 'Fira Code', monospace;
|
|
320
|
-
white-space: nowrap;
|
|
321
|
-
overflow: hidden;
|
|
322
|
-
text-overflow: ellipsis;
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
.agent-bar-wrap {
|
|
326
|
-
display: flex;
|
|
327
|
-
align-items: center;
|
|
328
|
-
gap: 6px;
|
|
329
|
-
margin-top: 4px;
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
.agent-bar-bg {
|
|
333
|
-
flex: 1;
|
|
334
|
-
height: 4px;
|
|
335
|
-
background: var(--border);
|
|
336
|
-
border-radius: 2px;
|
|
337
|
-
overflow: hidden;
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
.agent-bar-fill {
|
|
341
|
-
height: 100%;
|
|
342
|
-
border-radius: 2px;
|
|
343
|
-
transition: width 1s cubic-bezier(.4,0,.2,1);
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
.agent-platforms {
|
|
347
|
-
display: flex;
|
|
348
|
-
gap: 3px;
|
|
349
|
-
flex-wrap: wrap;
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
.platform-dot {
|
|
353
|
-
width: 6px;
|
|
354
|
-
height: 6px;
|
|
355
|
-
border-radius: 50%;
|
|
356
|
-
flex-shrink: 0;
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
.agent-count {
|
|
360
|
-
font-size: 14px;
|
|
361
|
-
font-weight: 700;
|
|
362
|
-
color: var(--text2);
|
|
363
|
-
min-width: 30px;
|
|
364
|
-
text-align: right;
|
|
365
|
-
font-variant-numeric: tabular-nums;
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
/* Source badge */
|
|
369
|
-
.source-badge {
|
|
370
|
-
font-size: 9px;
|
|
371
|
-
font-weight: 600;
|
|
372
|
-
text-transform: uppercase;
|
|
373
|
-
letter-spacing: .4px;
|
|
374
|
-
padding: 1px 5px;
|
|
375
|
-
border-radius: 3px;
|
|
376
|
-
background: var(--indigo-l);
|
|
377
|
-
color: var(--indigo);
|
|
378
|
-
flex-shrink: 0;
|
|
379
|
-
}
|
|
380
|
-
.source-badge.intent { background: var(--amber-l); color: var(--amber); }
|
|
381
|
-
|
|
382
|
-
/* ── Platform cards ─────────────────────────────────────────────────────── */
|
|
383
|
-
.platform-grid {
|
|
384
|
-
display: grid;
|
|
385
|
-
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
|
386
|
-
gap: 10px;
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
.platform-card {
|
|
390
|
-
border-radius: var(--radius-sm);
|
|
391
|
-
padding: 14px;
|
|
392
|
-
border: 1.5px solid var(--border);
|
|
393
|
-
background: var(--surface2);
|
|
394
|
-
transition: all .15s;
|
|
395
|
-
}
|
|
396
|
-
.platform-card:hover { box-shadow: var(--shadow); transform: translateY(-1px); }
|
|
397
|
-
.platform-card.active { border-color: currentColor; background: var(--surface); }
|
|
398
|
-
|
|
399
|
-
.platform-dot-lg {
|
|
400
|
-
width: 10px;
|
|
401
|
-
height: 10px;
|
|
402
|
-
border-radius: 50%;
|
|
403
|
-
display: inline-block;
|
|
404
|
-
margin-right: 6px;
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
.platform-name { font-size: 13px; font-weight: 700; margin-bottom: 6px; display: flex; align-items: center; }
|
|
408
|
-
.platform-sessions { font-size: 22px; font-weight: 800; color: var(--text); font-variant-numeric: tabular-nums; letter-spacing: -1px; }
|
|
409
|
-
.platform-label { font-size: 11px; color: var(--muted); margin-top: 2px; }
|
|
410
|
-
.platform-status {
|
|
411
|
-
font-size: 11px;
|
|
412
|
-
font-weight: 600;
|
|
413
|
-
margin-top: 6px;
|
|
414
|
-
display: inline-flex;
|
|
415
|
-
align-items: center;
|
|
416
|
-
gap: 4px;
|
|
417
|
-
}
|
|
418
|
-
.status-active { color: var(--green); }
|
|
419
|
-
.status-inactive { color: var(--subtle); }
|
|
420
|
-
.status-cloud { color: var(--blue); }
|
|
421
|
-
.cloud-badge {
|
|
422
|
-
font-size: 10px; font-weight: 600;
|
|
423
|
-
background: var(--blue-l); color: var(--blue);
|
|
424
|
-
padding: 1px 6px; border-radius: 10px; margin-top: 4px;
|
|
425
|
-
display: inline-block;
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
/* ── Cost chart ─────────────────────────────────────────────────────────── */
|
|
429
|
-
.chart-wrap { padding: 0 20px 20px; }
|
|
430
|
-
.chart-svg { width: 100%; overflow: visible; }
|
|
431
|
-
|
|
432
|
-
/* ── Sessions table ─────────────────────────────────────────────────────── */
|
|
433
|
-
.table-wrap { overflow-x: auto; }
|
|
434
|
-
|
|
435
|
-
table {
|
|
436
|
-
width: 100%;
|
|
437
|
-
border-collapse: collapse;
|
|
438
|
-
font-size: 13px;
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
th {
|
|
442
|
-
text-align: left;
|
|
443
|
-
padding: 10px 16px;
|
|
444
|
-
background: var(--surface2);
|
|
445
|
-
border-bottom: 1px solid var(--border);
|
|
446
|
-
font-size: 11px;
|
|
447
|
-
font-weight: 600;
|
|
448
|
-
text-transform: uppercase;
|
|
449
|
-
letter-spacing: .5px;
|
|
450
|
-
color: var(--muted);
|
|
451
|
-
white-space: nowrap;
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
td {
|
|
455
|
-
padding: 11px 16px;
|
|
456
|
-
border-bottom: 1px solid var(--border);
|
|
457
|
-
color: var(--text2);
|
|
458
|
-
vertical-align: middle;
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
tr:last-child td { border-bottom: none; }
|
|
462
|
-
tr:hover td { background: #f8f9ff; }
|
|
463
|
-
|
|
464
|
-
.td-project { font-weight: 600; color: var(--text); max-width: 160px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
465
|
-
.td-mono { font-family: 'SF Mono', 'Fira Code', monospace; font-size: 12px; }
|
|
466
|
-
|
|
467
|
-
.tag {
|
|
468
|
-
display: inline-flex;
|
|
469
|
-
align-items: center;
|
|
470
|
-
gap: 3px;
|
|
471
|
-
font-size: 11px;
|
|
472
|
-
font-weight: 600;
|
|
473
|
-
padding: 2px 8px;
|
|
474
|
-
border-radius: 20px;
|
|
475
|
-
white-space: nowrap;
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
/* ── Empty state ────────────────────────────────────────────────────────── */
|
|
479
|
-
.empty {
|
|
480
|
-
text-align: center;
|
|
481
|
-
padding: 40px 20px;
|
|
482
|
-
color: var(--muted);
|
|
483
|
-
}
|
|
484
|
-
.empty-icon { font-size: 36px; margin-bottom: 10px; }
|
|
485
|
-
.empty-title { font-size: 15px; font-weight: 600; color: var(--text2); margin-bottom: 6px; }
|
|
486
|
-
.empty-sub { font-size: 13px; }
|
|
487
|
-
|
|
488
|
-
/* ── Skeleton loader ────────────────────────────────────────────────────── */
|
|
489
|
-
.skeleton {
|
|
490
|
-
background: linear-gradient(90deg, #e2e8f0 25%, #f1f5f9 50%, #e2e8f0 75%);
|
|
491
|
-
background-size: 200% 100%;
|
|
492
|
-
animation: shimmer 1.5s infinite;
|
|
493
|
-
border-radius: 4px;
|
|
494
|
-
}
|
|
495
|
-
@keyframes shimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }
|
|
496
|
-
|
|
497
|
-
/* ── Tooltip ────────────────────────────────────────────────────────────── */
|
|
498
|
-
[data-tip] { position: relative; cursor: default; }
|
|
499
|
-
[data-tip]::after {
|
|
500
|
-
content: attr(data-tip);
|
|
501
|
-
position: absolute;
|
|
502
|
-
bottom: calc(100% + 6px);
|
|
503
|
-
left: 50%;
|
|
504
|
-
transform: translateX(-50%);
|
|
505
|
-
background: var(--text);
|
|
506
|
-
color: #fff;
|
|
507
|
-
font-size: 11px;
|
|
508
|
-
padding: 4px 8px;
|
|
509
|
-
border-radius: 5px;
|
|
510
|
-
white-space: nowrap;
|
|
511
|
-
opacity: 0;
|
|
512
|
-
pointer-events: none;
|
|
513
|
-
transition: opacity .15s;
|
|
514
|
-
z-index: 200;
|
|
515
|
-
}
|
|
516
|
-
[data-tip]:hover::after { opacity: 1; }
|
|
517
|
-
|
|
518
|
-
/* ── Error banner ───────────────────────────────────────────────────────── */
|
|
519
|
-
.error-banner {
|
|
520
|
-
background: var(--red-l);
|
|
521
|
-
border: 1px solid #fca5a5;
|
|
522
|
-
border-radius: var(--radius-sm);
|
|
523
|
-
padding: 12px 16px;
|
|
524
|
-
color: var(--red);
|
|
525
|
-
font-size: 13px;
|
|
526
|
-
margin-bottom: 16px;
|
|
527
|
-
display: none;
|
|
528
|
-
}
|
|
529
|
-
.error-banner.visible { display: block; }
|
|
530
|
-
|
|
531
|
-
/* ── Divider ────────────────────────────────────────────────────────────── */
|
|
532
|
-
.section-label {
|
|
533
|
-
font-size: 11px;
|
|
534
|
-
font-weight: 700;
|
|
535
|
-
text-transform: uppercase;
|
|
536
|
-
letter-spacing: .7px;
|
|
537
|
-
color: var(--muted);
|
|
538
|
-
padding: 8px 20px 12px;
|
|
539
|
-
border-bottom: 1px solid var(--border);
|
|
540
|
-
display: flex;
|
|
541
|
-
align-items: center;
|
|
542
|
-
gap: 6px;
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
/* ── Footer ─────────────────────────────────────────────────────────────── */
|
|
546
|
-
.footer {
|
|
547
|
-
text-align: center;
|
|
548
|
-
padding: 24px;
|
|
549
|
-
font-size: 12px;
|
|
550
|
-
color: var(--muted);
|
|
551
|
-
}
|
|
552
|
-
.footer a { color: var(--purple); text-decoration: none; }
|
|
553
|
-
.footer a:hover { text-decoration: underline; }
|
|
554
|
-
|
|
555
|
-
/* ── Scroll snap for leaderboard ────────────────────────────────────────── */
|
|
556
|
-
.agent-list-scroll {
|
|
557
|
-
max-height: 420px;
|
|
558
|
-
overflow-y: auto;
|
|
559
|
-
display: flex;
|
|
560
|
-
flex-direction: column;
|
|
561
|
-
gap: 6px;
|
|
562
|
-
padding-right: 4px;
|
|
563
|
-
}
|
|
564
|
-
.agent-list-scroll::-webkit-scrollbar { width: 4px; }
|
|
565
|
-
.agent-list-scroll::-webkit-scrollbar-track { background: transparent; }
|
|
566
|
-
.agent-list-scroll::-webkit-scrollbar-thumb { background: var(--border2); border-radius: 2px; }
|
|
567
|
-
|
|
568
|
-
/* ── Number animate ─────────────────────────────────────────────────────── */
|
|
569
|
-
.count-up { transition: all .6s cubic-bezier(.4,0,.2,1); }
|
|
570
|
-
|
|
571
|
-
/* Security warning */
|
|
572
|
-
.sec-warn {
|
|
573
|
-
background: #fef9c3;
|
|
574
|
-
border: 1px solid #fde047;
|
|
575
|
-
border-radius: var(--radius-sm);
|
|
576
|
-
padding: 12px 16px;
|
|
577
|
-
font-size: 13px;
|
|
578
|
-
color: #92400e;
|
|
579
|
-
margin-bottom: 16px;
|
|
580
|
-
display: flex;
|
|
581
|
-
align-items: flex-start;
|
|
582
|
-
gap: 10px;
|
|
583
|
-
}
|
|
584
|
-
</style>
|
|
585
|
-
</head>
|
|
586
|
-
<body>
|
|
587
|
-
|
|
588
|
-
<!-- ── Header ──────────────────────────────────────────────────────────────── -->
|
|
589
|
-
<header class="header">
|
|
590
|
-
<div class="logo">
|
|
591
|
-
<div class="logo-icon">KL</div>
|
|
592
|
-
<span class="logo-name">Kodelyth Lens</span>
|
|
593
|
-
<span class="logo-badge">v1.2</span>
|
|
594
|
-
</div>
|
|
595
|
-
|
|
596
|
-
<div class="header-spacer"></div>
|
|
597
|
-
|
|
598
|
-
<div class="updated-at" id="updatedAt">
|
|
599
|
-
<span class="live-dot"></span>
|
|
600
|
-
<span>Loading…</span>
|
|
601
|
-
</div>
|
|
602
|
-
|
|
603
|
-
<button class="btn-refresh" id="btnRefresh" onclick="refreshData(true)">
|
|
604
|
-
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
605
|
-
<polyline points="23 4 23 10 17 10"/><polyline points="1 20 1 14 7 14"/>
|
|
606
|
-
<path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/>
|
|
607
|
-
</svg>
|
|
608
|
-
Refresh
|
|
609
|
-
</button>
|
|
610
|
-
</header>
|
|
611
|
-
|
|
612
|
-
<!-- ── Main ────────────────────────────────────────────────────────────────── -->
|
|
613
|
-
<main class="main">
|
|
614
|
-
|
|
615
|
-
<div class="page-title">AI Coding Agent Usage</div>
|
|
616
|
-
<div class="page-sub" id="pageSub">Real-time tracking — detecting platforms…</div>
|
|
617
|
-
|
|
618
|
-
<div class="error-banner" id="errorBanner"></div>
|
|
619
|
-
|
|
620
|
-
<!-- Stat cards -->
|
|
621
|
-
<div class="stats-grid" id="statsGrid">
|
|
622
|
-
<div class="stat-card">
|
|
623
|
-
<div class="stat-label">
|
|
624
|
-
<div class="stat-icon" style="background:#ede9fe">🧠</div>
|
|
625
|
-
Agent Calls (All Time)
|
|
626
|
-
</div>
|
|
627
|
-
<div class="stat-value count-up" id="s-agentAll">—</div>
|
|
628
|
-
<div class="stat-sub">across all platforms</div>
|
|
629
|
-
</div>
|
|
630
|
-
<div class="stat-card">
|
|
631
|
-
<div class="stat-label">
|
|
632
|
-
<div class="stat-icon" style="background:#dbeafe">💬</div>
|
|
633
|
-
Total Sessions
|
|
634
|
-
</div>
|
|
635
|
-
<div class="stat-value count-up" id="s-sessAll">—</div>
|
|
636
|
-
<div class="stat-sub" id="s-sessToday">— today</div>
|
|
637
|
-
</div>
|
|
638
|
-
<div class="stat-card">
|
|
639
|
-
<div class="stat-label">
|
|
640
|
-
<div class="stat-icon" style="background:#d1fae5">⚡</div>
|
|
641
|
-
Cache Hit Rate
|
|
642
|
-
</div>
|
|
643
|
-
<div class="stat-value count-up" id="s-cache">—</div>
|
|
644
|
-
<div class="stat-sub" id="s-cacheSaved">— saved</div>
|
|
645
|
-
</div>
|
|
646
|
-
<div class="stat-card">
|
|
647
|
-
<div class="stat-label">
|
|
648
|
-
<div class="stat-icon" style="background:#fef3c7">💰</div>
|
|
649
|
-
Actual Cost Paid
|
|
650
|
-
</div>
|
|
651
|
-
<div class="stat-value count-up" id="s-costAll">—</div>
|
|
652
|
-
<div class="stat-sub" id="s-costToday">— today</div>
|
|
653
|
-
</div>
|
|
654
|
-
<div class="stat-card">
|
|
655
|
-
<div class="stat-label">
|
|
656
|
-
<div class="stat-icon" style="background:#cffafe">🔁</div>
|
|
657
|
-
Tokens Used
|
|
658
|
-
</div>
|
|
659
|
-
<div class="stat-value count-up" id="s-tokAll">—</div>
|
|
660
|
-
<div class="stat-sub" id="s-tokToday">— today</div>
|
|
661
|
-
</div>
|
|
662
|
-
</div>
|
|
663
|
-
|
|
664
|
-
<!-- ECC Savings banner -->
|
|
665
|
-
<div id="savingsBanner" style="display:none;margin-bottom:20px">
|
|
666
|
-
<div style="background:linear-gradient(135deg,#f0fdf4,#ede9fe);border:1px solid #bbf7d0;
|
|
667
|
-
border-radius:var(--radius);padding:20px 24px;display:flex;align-items:center;gap:0;flex-wrap:wrap">
|
|
668
|
-
<div style="flex:1;min-width:160px;text-align:center;padding:8px 16px;border-right:1px solid #d1fae5">
|
|
669
|
-
<div style="font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.6px;color:#64748b;margin-bottom:6px">Without ECC (est.)</div>
|
|
670
|
-
<div style="font-size:28px;font-weight:800;color:#dc2626;letter-spacing:-1px" id="sv-without">—</div>
|
|
671
|
-
<div style="font-size:12px;color:#94a3b8;margin-top:2px">full token price, no cache</div>
|
|
672
|
-
</div>
|
|
673
|
-
<div style="flex:1;min-width:160px;text-align:center;padding:8px 16px;border-right:1px solid #d1fae5">
|
|
674
|
-
<div style="font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.6px;color:#64748b;margin-bottom:6px">With ECC (actual)</div>
|
|
675
|
-
<div style="font-size:28px;font-weight:800;color:#059669;letter-spacing:-1px" id="sv-actual">—</div>
|
|
676
|
-
<div style="font-size:12px;color:#94a3b8;margin-top:2px">cache + agent efficiency</div>
|
|
677
|
-
</div>
|
|
678
|
-
<div style="flex:1;min-width:160px;text-align:center;padding:8px 16px;border-right:1px solid #ede9fe">
|
|
679
|
-
<div style="font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.6px;color:#7c3aed;margin-bottom:6px">💰 You Saved</div>
|
|
680
|
-
<div style="font-size:28px;font-weight:800;color:#7c3aed;letter-spacing:-1px" id="sv-saved">—</div>
|
|
681
|
-
<div style="font-size:12px;color:#94a3b8;margin-top:2px" id="sv-pct">— reduction</div>
|
|
682
|
-
</div>
|
|
683
|
-
<div style="flex:1.5;min-width:200px;padding:8px 16px">
|
|
684
|
-
<div style="font-size:12px;font-weight:600;color:#334155;margin-bottom:8px">Cache hit rate</div>
|
|
685
|
-
<div style="background:#e2e8f0;border-radius:4px;height:10px;overflow:hidden">
|
|
686
|
-
<div id="sv-bar" style="height:100%;background:linear-gradient(90deg,#7c3aed,#059669);border-radius:4px;transition:width 1s ease"></div>
|
|
687
|
-
</div>
|
|
688
|
-
<div style="display:flex;justify-content:space-between;margin-top:4px">
|
|
689
|
-
<span style="font-size:11px;color:#dc2626" id="sv-bar-paid">paid</span>
|
|
690
|
-
<span style="font-size:11px;color:#7c3aed;font-weight:700" id="sv-bar-rate">— hit rate</span>
|
|
691
|
-
<span style="font-size:11px;color:#059669" id="sv-bar-saved">saved</span>
|
|
692
|
-
</div>
|
|
693
|
-
<div style="font-size:11px;color:#94a3b8;margin-top:6px">Your numbers · from real session data · updates live</div>
|
|
694
|
-
</div>
|
|
695
|
-
</div>
|
|
696
|
-
</div>
|
|
697
|
-
|
|
698
|
-
<!-- Agent leaderboard + Platform status -->
|
|
699
|
-
<div class="three-col">
|
|
700
|
-
<div class="card">
|
|
701
|
-
<div class="card-header">
|
|
702
|
-
<div class="card-title">
|
|
703
|
-
<div class="card-title-icon" style="background:#ede9fe">🏆</div>
|
|
704
|
-
Agent Leaderboard
|
|
705
|
-
</div>
|
|
706
|
-
<span class="card-sub" id="agentTotal">Loading…</span>
|
|
707
|
-
</div>
|
|
708
|
-
<div class="card-body">
|
|
709
|
-
<div class="agent-list-scroll" id="agentList">
|
|
710
|
-
<div class="empty"><div class="empty-icon">⏳</div><div class="empty-title">Loading agents…</div></div>
|
|
711
|
-
</div>
|
|
712
|
-
</div>
|
|
713
|
-
</div>
|
|
714
|
-
|
|
715
|
-
<div class="card">
|
|
716
|
-
<div class="card-header">
|
|
717
|
-
<div class="card-title">
|
|
718
|
-
<div class="card-title-icon" style="background:#d1fae5">🌐</div>
|
|
719
|
-
Platforms
|
|
720
|
-
</div>
|
|
721
|
-
</div>
|
|
722
|
-
<div class="card-body">
|
|
723
|
-
<div class="platform-grid" id="platformGrid">
|
|
724
|
-
<div class="empty"><div class="empty-icon">⏳</div></div>
|
|
725
|
-
</div>
|
|
726
|
-
</div>
|
|
727
|
-
</div>
|
|
728
|
-
</div>
|
|
729
|
-
|
|
730
|
-
<!-- Windsurf Quota Panel (hidden until data loads) -->
|
|
731
|
-
<div class="card" id="windsurfQuota" style="display:none;margin-bottom:0">
|
|
732
|
-
<div class="card-header" style="padding-bottom:8px">
|
|
733
|
-
<div class="card-title">
|
|
734
|
-
<div class="card-title-icon" style="background:#cffafe">🏄</div>
|
|
735
|
-
Windsurf Quota Usage
|
|
736
|
-
</div>
|
|
737
|
-
</div>
|
|
738
|
-
<div class="card-body"></div>
|
|
739
|
-
</div>
|
|
740
|
-
|
|
741
|
-
<!-- Cost chart + Recent sessions -->
|
|
742
|
-
<div class="two-col">
|
|
743
|
-
<div class="card">
|
|
744
|
-
<div class="card-header">
|
|
745
|
-
<div class="card-title">
|
|
746
|
-
<div class="card-title-icon" style="background:#fef3c7">📈</div>
|
|
747
|
-
Cost — Last 30 Days
|
|
748
|
-
</div>
|
|
749
|
-
</div>
|
|
750
|
-
<div class="chart-wrap">
|
|
751
|
-
<svg id="costChart" class="chart-svg" height="140"></svg>
|
|
752
|
-
</div>
|
|
753
|
-
</div>
|
|
754
|
-
|
|
755
|
-
<div class="card">
|
|
756
|
-
<div class="card-header">
|
|
757
|
-
<div class="card-title">
|
|
758
|
-
<div class="card-title-icon" style="background:#cffafe">📅</div>
|
|
759
|
-
Today
|
|
760
|
-
</div>
|
|
761
|
-
</div>
|
|
762
|
-
<div class="card-body">
|
|
763
|
-
<div style="display:flex;flex-direction:column;gap:12px" id="todayStats">
|
|
764
|
-
<div class="empty"><div class="empty-icon">⏳</div><div class="empty-title">Loading…</div></div>
|
|
765
|
-
</div>
|
|
766
|
-
</div>
|
|
767
|
-
</div>
|
|
768
|
-
</div>
|
|
769
|
-
|
|
770
|
-
<!-- Recent sessions table -->
|
|
771
|
-
<div class="card" style="margin-bottom:20px">
|
|
772
|
-
<div class="section-label">
|
|
773
|
-
🕐 Recent Sessions
|
|
774
|
-
<span style="margin-left:auto;font-size:11px;color:var(--subtle);text-transform:none;letter-spacing:0;font-weight:400">latest 50</span>
|
|
775
|
-
</div>
|
|
776
|
-
<div class="table-wrap">
|
|
777
|
-
<table>
|
|
778
|
-
<thead>
|
|
779
|
-
<tr>
|
|
780
|
-
<th>Project</th>
|
|
781
|
-
<th>Platform</th>
|
|
782
|
-
<th>Agents</th>
|
|
783
|
-
<th>Tokens</th>
|
|
784
|
-
<th>Cost</th>
|
|
785
|
-
<th>Date</th>
|
|
786
|
-
</tr>
|
|
787
|
-
</thead>
|
|
788
|
-
<tbody id="sessionsTbody">
|
|
789
|
-
<tr><td colspan="6"><div class="empty"><div class="empty-icon">⏳</div><div class="empty-title">Loading…</div></div></td></tr>
|
|
790
|
-
</tbody>
|
|
791
|
-
</table>
|
|
792
|
-
</div>
|
|
793
|
-
</div>
|
|
794
|
-
|
|
795
|
-
</main>
|
|
796
|
-
|
|
797
|
-
<footer class="footer">
|
|
798
|
-
Kodelyth Lens — Part of <a href="https://github.com/sifxprime/kodelyth-ecc" target="_blank">Kodelyth ECC</a> · Open source · MIT
|
|
799
|
-
</footer>
|
|
800
|
-
|
|
801
|
-
<script>
|
|
802
|
-
// ── Constants ─────────────────────────────────────────────────────────────────
|
|
803
|
-
const PLATFORM_META = {
|
|
804
|
-
claude: { label: 'Claude Code', color: '#f97316', icon: '🤖' },
|
|
805
|
-
cursor: { label: 'Cursor', color: '#3b82f6', icon: '✏️' },
|
|
806
|
-
windsurf: { label: 'Windsurf', color: '#06b6d4', icon: '🏄' },
|
|
807
|
-
codex: { label: 'Codex CLI', color: '#10b981', icon: '⚡' },
|
|
808
|
-
opencode: { label: 'OpenCode', color: '#f43f5e', icon: '🔴' },
|
|
809
|
-
antigravity: { label: 'Antigravity', color: '#7c3aed', icon: '🚀' },
|
|
810
|
-
};
|
|
811
|
-
|
|
812
|
-
let _data = null;
|
|
813
|
-
let _refreshTimer = null;
|
|
814
|
-
|
|
815
|
-
// ── Fetch + render ─────────────────────────────────────────────────────────────
|
|
816
|
-
async function refreshData(force = false) {
|
|
817
|
-
const btn = document.getElementById('btnRefresh');
|
|
818
|
-
btn.classList.add('spinning');
|
|
819
|
-
|
|
820
|
-
try {
|
|
821
|
-
const url = force ? '/api/all?refresh=1' : '/api/all';
|
|
822
|
-
const res = await fetch(url);
|
|
823
|
-
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
824
|
-
_data = await res.json();
|
|
825
|
-
renderAll(_data);
|
|
826
|
-
document.getElementById('errorBanner').classList.remove('visible');
|
|
827
|
-
} catch (err) {
|
|
828
|
-
document.getElementById('errorBanner').textContent = `⚠ Failed to load data: ${err.message}`;
|
|
829
|
-
document.getElementById('errorBanner').classList.add('visible');
|
|
830
|
-
} finally {
|
|
831
|
-
btn.classList.remove('spinning');
|
|
832
|
-
}
|
|
833
|
-
}
|
|
834
|
-
|
|
835
|
-
function renderAll(d) {
|
|
836
|
-
renderStats(d.stats);
|
|
837
|
-
renderAgents(d.agents);
|
|
838
|
-
renderPlatforms(d.platforms, d.windsurf);
|
|
839
|
-
renderCostChart(d.costs);
|
|
840
|
-
renderToday(d.stats);
|
|
841
|
-
renderSessions(d.sessions);
|
|
842
|
-
renderSavings(d.stats);
|
|
843
|
-
renderWindsurfQuota(d.windsurf?.quotaData);
|
|
844
|
-
|
|
845
|
-
// Dynamic subtitle — list installed platform names
|
|
846
|
-
if (d.platforms) {
|
|
847
|
-
const installed = Object.entries(d.platforms)
|
|
848
|
-
.filter(([, p]) => p.installed)
|
|
849
|
-
.map(([, p]) => p.label);
|
|
850
|
-
const sub = installed.length
|
|
851
|
-
? `Tracking ${installed.join(' · ')}`
|
|
852
|
-
: 'Real-time tracking — detecting platforms…';
|
|
853
|
-
setEl('pageSub', sub);
|
|
854
|
-
}
|
|
855
|
-
|
|
856
|
-
const ts = d.updatedAt ? new Date(d.updatedAt).toLocaleTimeString() : 'just now';
|
|
857
|
-
document.getElementById('updatedAt').innerHTML =
|
|
858
|
-
`<span class="live-dot"></span><span>Updated ${ts}</span>`;
|
|
859
|
-
}
|
|
860
|
-
|
|
861
|
-
// ── Stats ─────────────────────────────────────────────────────────────────────
|
|
862
|
-
function renderStats(s) {
|
|
863
|
-
const at = s.allTime, td = s.today;
|
|
864
|
-
setEl('s-agentAll', fmtNum(at.agentCalls));
|
|
865
|
-
setEl('s-sessAll', fmtNum(at.sessions));
|
|
866
|
-
setEl('s-sessToday', `${fmtNum(td.sessions)} today`);
|
|
867
|
-
setEl('s-cache', `${at.cacheHitRate}%`);
|
|
868
|
-
setEl('s-cacheSaved', `$${at.cacheSaved.toFixed(4)} saved`);
|
|
869
|
-
setEl('s-costAll', `$${at.cost.toFixed(4)}`);
|
|
870
|
-
setEl('s-costToday', `$${td.cost.toFixed(4)} today`);
|
|
871
|
-
setEl('s-tokAll', fmtTokens(at.tokens));
|
|
872
|
-
setEl('s-tokToday', `${fmtTokens(td.tokens)} today`);
|
|
873
|
-
}
|
|
874
|
-
|
|
875
|
-
function renderToday(s) {
|
|
876
|
-
const td = s.today;
|
|
877
|
-
const rows = [
|
|
878
|
-
['Sessions', fmtNum(td.sessions), '#3b82f6'],
|
|
879
|
-
['Agent calls', fmtNum(td.agentCalls), '#7c3aed'],
|
|
880
|
-
['Tokens', fmtTokens(td.tokens), '#0891b2'],
|
|
881
|
-
['Cost', `$${td.cost.toFixed(4)}`, '#d97706'],
|
|
882
|
-
];
|
|
883
|
-
document.getElementById('todayStats').innerHTML = rows.map(([label, val, color]) => `
|
|
884
|
-
<div style="display:flex;justify-content:space-between;align-items:center;
|
|
885
|
-
padding:12px;background:var(--surface2);border-radius:var(--radius-sm);
|
|
886
|
-
border:1px solid var(--border)">
|
|
887
|
-
<span style="font-size:13px;color:var(--muted);font-weight:500">${label}</span>
|
|
888
|
-
<span style="font-size:18px;font-weight:800;color:${color};font-variant-numeric:tabular-nums">${val}</span>
|
|
889
|
-
</div>
|
|
890
|
-
`).join('');
|
|
891
|
-
}
|
|
892
|
-
|
|
893
|
-
// ── Agent leaderboard ─────────────────────────────────────────────────────────
|
|
894
|
-
function renderAgents(agents) {
|
|
895
|
-
const el = document.getElementById('agentList');
|
|
896
|
-
if (!agents || !agents.length) {
|
|
897
|
-
el.innerHTML = `<div class="empty"><div class="empty-icon">🤔</div><div class="empty-title">No agent data yet</div><div class="empty-sub">Start using agents in Claude Code or Codex</div></div>`;
|
|
898
|
-
document.getElementById('agentTotal').textContent = '0 agents';
|
|
899
|
-
return;
|
|
900
|
-
}
|
|
901
|
-
document.getElementById('agentTotal').textContent = `${agents.length} agents tracked`;
|
|
902
|
-
const max = agents[0]?.calls || 1;
|
|
903
|
-
|
|
904
|
-
el.innerHTML = agents.slice(0, 30).map((a, i) => {
|
|
905
|
-
const pct = Math.max(4, (a.calls / max) * 100).toFixed(1);
|
|
906
|
-
const topCls = i < 3 ? 'top' : '';
|
|
907
|
-
const platformDots = (a.platforms || []).map(p => {
|
|
908
|
-
const meta = PLATFORM_META[p] || { color: '#94a3b8' };
|
|
909
|
-
return `<span class="platform-dot" style="background:${meta.color}" data-tip="${PLATFORM_META[p]?.label || p}"></span>`;
|
|
910
|
-
}).join('');
|
|
911
|
-
|
|
912
|
-
return `
|
|
913
|
-
<div class="agent-row">
|
|
914
|
-
<span class="agent-rank ${topCls}">${i + 1}</span>
|
|
915
|
-
<div class="agent-info">
|
|
916
|
-
<div class="agent-name" style="color:${a.color || '#334155'}">${a.name}</div>
|
|
917
|
-
<div class="agent-bar-wrap">
|
|
918
|
-
<div class="agent-bar-bg">
|
|
919
|
-
<div class="agent-bar-fill" style="width:${pct}%;background:${a.color || '#334155'}20;
|
|
920
|
-
border: 1px solid ${a.color || '#334155'}40;box-sizing:border-box"></div>
|
|
921
|
-
</div>
|
|
922
|
-
<div class="agent-platforms">${platformDots}</div>
|
|
923
|
-
</div>
|
|
924
|
-
</div>
|
|
925
|
-
<span class="agent-count" style="color:${a.color || '#334155'}">${fmtNum(a.calls)}</span>
|
|
926
|
-
</div>`;
|
|
927
|
-
}).join('');
|
|
928
|
-
}
|
|
929
|
-
|
|
930
|
-
// Cloud AI platforms — store conversations server-side (no local token data)
|
|
931
|
-
const CLOUD_AI_PLATFORMS = new Set(['antigravity', 'windsurf']);
|
|
932
|
-
|
|
933
|
-
// ── Platforms ─────────────────────────────────────────────────────────────────
|
|
934
|
-
function renderPlatforms(platforms, windsurfData) {
|
|
935
|
-
const grid = document.getElementById('platformGrid');
|
|
936
|
-
if (!platforms) { grid.innerHTML = '<div class="empty"><div class="empty-icon">❌</div></div>'; return; }
|
|
937
|
-
|
|
938
|
-
const installedEntries = Object.entries(platforms).filter(([, p]) => p.installed);
|
|
939
|
-
if (!installedEntries.length) {
|
|
940
|
-
grid.innerHTML = '<div class="empty"><div class="empty-title">No platforms detected</div></div>';
|
|
941
|
-
return;
|
|
942
|
-
}
|
|
943
|
-
grid.innerHTML = installedEntries.map(([key, p]) => {
|
|
944
|
-
const meta = PLATFORM_META[key] || { label: key, color: '#94a3b8', icon: '💻' };
|
|
945
|
-
const cloudAI = CLOUD_AI_PLATFORMS.has(key);
|
|
946
|
-
const active = p.installed && p.sessions > 0;
|
|
947
|
-
const present = p.installed;
|
|
948
|
-
|
|
949
|
-
let statusClass, statusText;
|
|
950
|
-
if (!present) {
|
|
951
|
-
statusClass = 'status-inactive'; statusText = '○ Not found';
|
|
952
|
-
} else if (cloudAI) {
|
|
953
|
-
statusClass = 'status-cloud'; statusText = '● Installed';
|
|
954
|
-
} else if (active) {
|
|
955
|
-
statusClass = 'status-active'; statusText = '● Active';
|
|
956
|
-
} else {
|
|
957
|
-
statusClass = 'status-inactive'; statusText = '○ Installed';
|
|
958
|
-
}
|
|
959
|
-
|
|
960
|
-
let cloudBadge = '';
|
|
961
|
-
let sessDisplay, sessLabel;
|
|
962
|
-
if (key === 'windsurf' && present) {
|
|
963
|
-
const q = windsurfData?.quotaData;
|
|
964
|
-
if (q) {
|
|
965
|
-
const msgPct = q.totalMessages > 0 ? Math.round((q.usedMessages / q.totalMessages) * 100) : q.dailyUsedPercent;
|
|
966
|
-
sessDisplay = `${q.usedMessages.toLocaleString()} / ${q.totalMessages.toLocaleString()}`;
|
|
967
|
-
sessLabel = 'messages used';
|
|
968
|
-
cloudBadge = `
|
|
969
|
-
<div style="margin-top:8px;font-size:11px;color:var(--muted)">
|
|
970
|
-
<div style="display:flex;justify-content:space-between;margin-bottom:3px">
|
|
971
|
-
<span>${q.planName} plan</span><span>${q.dailyUsedPercent}% daily used</span>
|
|
972
|
-
</div>
|
|
973
|
-
<div style="background:#e2e8f0;border-radius:4px;height:4px;overflow:hidden">
|
|
974
|
-
<div style="background:${meta.color};height:4px;width:${Math.min(q.dailyUsedPercent,100)}%;transition:width .3s"></div>
|
|
975
|
-
</div>
|
|
976
|
-
<div style="display:flex;justify-content:space-between;margin-top:4px">
|
|
977
|
-
<span>Flow: ${q.usedFlowActions} / ${q.totalFlowActions}</span><span>${q.weeklyUsedPercent}% weekly</span>
|
|
978
|
-
</div>
|
|
979
|
-
</div>`;
|
|
980
|
-
} else {
|
|
981
|
-
sessDisplay = p.sessions > 0 ? fmtNum(p.sessions) + ' proj' : '—';
|
|
982
|
-
sessLabel = 'ECC projects';
|
|
983
|
-
cloudBadge = `<div class="cloud-badge">☁ Quota data loading…</div>`;
|
|
984
|
-
}
|
|
985
|
-
} else if (cloudAI && present) {
|
|
986
|
-
sessDisplay = p.sessions > 0 ? fmtNum(p.sessions) + ' proj' : '—';
|
|
987
|
-
sessLabel = 'ECC projects';
|
|
988
|
-
cloudBadge = `<div class="cloud-badge">☁ Cloud AI sessions</div>`;
|
|
989
|
-
} else {
|
|
990
|
-
sessDisplay = fmtNum(p.sessions);
|
|
991
|
-
sessLabel = 'sessions';
|
|
992
|
-
}
|
|
993
|
-
|
|
994
|
-
return `
|
|
995
|
-
<div class="platform-card ${active || (cloudAI && present) ? 'active' : ''}" style="color:${meta.color}">
|
|
996
|
-
<div class="platform-name">
|
|
997
|
-
<span class="platform-dot-lg" style="background:${meta.color}"></span>
|
|
998
|
-
${meta.label}
|
|
999
|
-
</div>
|
|
1000
|
-
<div class="platform-sessions" style="color:${(active || (cloudAI && present)) ? meta.color : 'var(--muted)'};font-size:${key==='windsurf'&&windsurfData?.quotaData?'14px':'22px'}">${sessDisplay}</div>
|
|
1001
|
-
<div class="platform-label">${sessLabel}</div>
|
|
1002
|
-
<div class="platform-status ${statusClass}">${statusText}</div>
|
|
1003
|
-
${cloudBadge}
|
|
1004
|
-
</div>`;
|
|
1005
|
-
}).join('');
|
|
1006
|
-
}
|
|
1007
|
-
|
|
1008
|
-
// ── Windsurf Quota Panel ─────────────────────────────────────────────────────
|
|
1009
|
-
function renderWindsurfQuota(q) {
|
|
1010
|
-
const el = document.getElementById('windsurfQuota');
|
|
1011
|
-
if (!el) return;
|
|
1012
|
-
if (!q) { el.style.display = 'none'; return; }
|
|
1013
|
-
el.style.display = 'block';
|
|
1014
|
-
el.querySelector('.card-body').innerHTML = `
|
|
1015
|
-
<div style="display:flex;gap:24px;flex-wrap:wrap;align-items:center">
|
|
1016
|
-
<div>
|
|
1017
|
-
<div style="font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:.5px">Windsurf ${q.planName}</div>
|
|
1018
|
-
<div style="font-size:22px;font-weight:800;color:#06b6d4;font-variant-numeric:tabular-nums">${q.usedMessages.toLocaleString()} <span style="font-size:14px;font-weight:400;color:var(--muted)">/ ${q.totalMessages.toLocaleString()} msgs</span></div>
|
|
1019
|
-
</div>
|
|
1020
|
-
<div style="flex:1;min-width:160px">
|
|
1021
|
-
<div style="display:flex;justify-content:space-between;font-size:11px;color:var(--muted);margin-bottom:4px">
|
|
1022
|
-
<span>Daily quota</span><span>${q.dailyUsedPercent}% used</span>
|
|
1023
|
-
</div>
|
|
1024
|
-
<div style="background:#e2e8f0;border-radius:6px;height:8px;overflow:hidden">
|
|
1025
|
-
<div style="background:#06b6d4;height:8px;width:${Math.min(q.dailyUsedPercent,100)}%;border-radius:6px;transition:width .4s"></div>
|
|
1026
|
-
</div>
|
|
1027
|
-
<div style="display:flex;justify-content:space-between;font-size:11px;color:var(--muted);margin-top:6px">
|
|
1028
|
-
<span>Weekly quota</span><span>${q.weeklyUsedPercent}% used</span>
|
|
1029
|
-
</div>
|
|
1030
|
-
<div style="background:#e2e8f0;border-radius:6px;height:8px;overflow:hidden;margin-top:4px">
|
|
1031
|
-
<div style="background:#0e7490;height:8px;width:${Math.min(q.weeklyUsedPercent,100)}%;border-radius:6px;transition:width .4s"></div>
|
|
1032
|
-
</div>
|
|
1033
|
-
</div>
|
|
1034
|
-
<div style="text-align:right">
|
|
1035
|
-
<div style="font-size:11px;color:var(--muted)">Flow Actions</div>
|
|
1036
|
-
<div style="font-size:16px;font-weight:700;color:#06b6d4">${q.usedFlowActions.toLocaleString()} <span style="font-size:12px;color:var(--muted)">/ ${q.totalFlowActions.toLocaleString()}</span></div>
|
|
1037
|
-
${q.endDate ? `<div style="font-size:10px;color:var(--muted);margin-top:2px">Resets ${q.endDate}</div>` : ''}
|
|
1038
|
-
</div>
|
|
1039
|
-
</div>`;
|
|
1040
|
-
}
|
|
1041
|
-
|
|
1042
|
-
// ── Cost chart ────────────────────────────────────────────────────────────────
|
|
1043
|
-
function renderCostChart(dailyCosts) {
|
|
1044
|
-
const svg = document.getElementById('costChart');
|
|
1045
|
-
if (!dailyCosts || !dailyCosts.length) {
|
|
1046
|
-
svg.innerHTML = `<text x="50%" y="70" text-anchor="middle" fill="#94a3b8" font-size="13">No cost data yet</text>`;
|
|
1047
|
-
return;
|
|
1048
|
-
}
|
|
1049
|
-
|
|
1050
|
-
const W = svg.parentElement.clientWidth - 40;
|
|
1051
|
-
const H = 120;
|
|
1052
|
-
svg.setAttribute('viewBox', `0 0 ${W} ${H}`);
|
|
1053
|
-
|
|
1054
|
-
const maxCost = Math.max(...dailyCosts.map(d => d.cost), 0.0001);
|
|
1055
|
-
const pts = dailyCosts.slice(-30);
|
|
1056
|
-
const step = W / Math.max(pts.length - 1, 1);
|
|
1057
|
-
|
|
1058
|
-
const coords = pts.map((d, i) => ({
|
|
1059
|
-
x: i * step,
|
|
1060
|
-
y: H - 10 - ((d.cost / maxCost) * (H - 20)),
|
|
1061
|
-
cost: d.cost,
|
|
1062
|
-
date: d.date,
|
|
1063
|
-
}));
|
|
1064
|
-
|
|
1065
|
-
const area = coords.map((c, i) => `${i === 0 ? 'M' : 'L'}${c.x.toFixed(1)},${c.y.toFixed(1)}`).join(' ')
|
|
1066
|
-
+ ` L${coords[coords.length-1].x.toFixed(1)},${H} L0,${H} Z`;
|
|
1067
|
-
|
|
1068
|
-
const line = coords.map((c, i) => `${i === 0 ? 'M' : 'L'}${c.x.toFixed(1)},${c.y.toFixed(1)}`).join(' ');
|
|
1069
|
-
|
|
1070
|
-
const id = 'chartGrad';
|
|
1071
|
-
svg.innerHTML = `
|
|
1072
|
-
<defs>
|
|
1073
|
-
<linearGradient id="${id}" x1="0" y1="0" x2="0" y2="1">
|
|
1074
|
-
<stop offset="0%" stop-color="#7c3aed" stop-opacity=".15"/>
|
|
1075
|
-
<stop offset="100%" stop-color="#7c3aed" stop-opacity="0"/>
|
|
1076
|
-
</linearGradient>
|
|
1077
|
-
</defs>
|
|
1078
|
-
<!-- Grid lines -->
|
|
1079
|
-
${[0.25, 0.5, 0.75, 1].map(f => {
|
|
1080
|
-
const y = H - 10 - f * (H - 20);
|
|
1081
|
-
const label = '$' + (maxCost * f).toFixed(4);
|
|
1082
|
-
return `<line x1="0" y1="${y.toFixed(1)}" x2="${W}" y2="${y.toFixed(1)}" stroke="#e2e8f0" stroke-width="1"/>
|
|
1083
|
-
<text x="0" y="${(y - 3).toFixed(1)}" fill="#94a3b8" font-size="9">${label}</text>`;
|
|
1084
|
-
}).join('')}
|
|
1085
|
-
<!-- Area fill -->
|
|
1086
|
-
<path d="${area}" fill="url(#${id})"/>
|
|
1087
|
-
<!-- Line -->
|
|
1088
|
-
<path d="${line}" fill="none" stroke="#7c3aed" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
1089
|
-
<!-- Dots -->
|
|
1090
|
-
${coords.map(c => `
|
|
1091
|
-
<circle cx="${c.x.toFixed(1)}" cy="${c.y.toFixed(1)}" r="3" fill="#7c3aed" stroke="#fff" stroke-width="1.5">
|
|
1092
|
-
<title>${c.date}: $${c.cost.toFixed(6)}</title>
|
|
1093
|
-
</circle>`).join('')}
|
|
1094
|
-
`;
|
|
1095
|
-
}
|
|
1096
|
-
|
|
1097
|
-
// ── Sessions table ─────────────────────────────────────────────────────────────
|
|
1098
|
-
function renderSessions(sessions) {
|
|
1099
|
-
const tbody = document.getElementById('sessionsTbody');
|
|
1100
|
-
if (!sessions || !sessions.length) {
|
|
1101
|
-
tbody.innerHTML = `<tr><td colspan="6"><div class="empty">
|
|
1102
|
-
<div class="empty-icon">📭</div>
|
|
1103
|
-
<div class="empty-title">No sessions found</div>
|
|
1104
|
-
<div class="empty-sub">Run Claude Code, Codex, or Windsurf to see sessions here</div>
|
|
1105
|
-
</div></td></tr>`;
|
|
1106
|
-
return;
|
|
1107
|
-
}
|
|
1108
|
-
|
|
1109
|
-
// Filter out cloud-AI placeholder sessions (no real token data)
|
|
1110
|
-
const realSessions = sessions.filter(s => !s.cloudAI || s.tokens?.total > 0);
|
|
1111
|
-
if (!realSessions.length) {
|
|
1112
|
-
tbody.innerHTML = `<tr><td colspan="6"><div class="empty">
|
|
1113
|
-
<div class="empty-icon">📭</div>
|
|
1114
|
-
<div class="empty-title">No sessions found</div>
|
|
1115
|
-
<div class="empty-sub">Run Claude Code or Codex to see sessions here</div>
|
|
1116
|
-
</div></td></tr>`;
|
|
1117
|
-
return;
|
|
1118
|
-
}
|
|
1119
|
-
tbody.innerHTML = realSessions.slice(0, 50).map(s => {
|
|
1120
|
-
const meta = PLATFORM_META[s.platform] || { label: s.platform, color: '#94a3b8' };
|
|
1121
|
-
const agents = (s.agents || []).slice(0, 3).map(a => {
|
|
1122
|
-
const badge = a.source === 'intent' ? ' title="auto-detected by intent"' : '';
|
|
1123
|
-
return `<span class="tag"${badge} style="background:${hexLight(a.agent)}1a;color:${hexLight(a.agent)}">${a.agent}</span>`;
|
|
1124
|
-
}).join('');
|
|
1125
|
-
const moreAgents = (s.agents?.length || 0) > 3 ? `<span class="tag" style="background:var(--surface2);color:var(--muted)">+${s.agents.length - 3}</span>` : '';
|
|
1126
|
-
|
|
1127
|
-
return `<tr>
|
|
1128
|
-
<td class="td-project" title="${s.project}">${s.project || '—'}</td>
|
|
1129
|
-
<td>
|
|
1130
|
-
<span class="tag" style="background:${meta.color}1a;color:${meta.color}">
|
|
1131
|
-
${meta.label}
|
|
1132
|
-
</span>
|
|
1133
|
-
</td>
|
|
1134
|
-
<td style="max-width:220px">
|
|
1135
|
-
<div style="display:flex;flex-wrap:wrap;gap:4px">${agents}${moreAgents}</div>
|
|
1136
|
-
</td>
|
|
1137
|
-
<td class="td-mono">${s.cost === null ? '<span style="color:#94a3b8">quota</span>' : fmtTokens(s.tokens?.total || 0)}</td>
|
|
1138
|
-
<td class="td-mono">${s.cost === null ? '<span style="color:#94a3b8">—</span>' : s.cost > 0 ? '$' + s.cost.toFixed(4) : '—'}</td>
|
|
1139
|
-
<td style="white-space:nowrap;color:var(--muted)">${fmtDate(s.date)}</td>
|
|
1140
|
-
</tr>`;
|
|
1141
|
-
}).join('');
|
|
1142
|
-
}
|
|
1143
|
-
|
|
1144
|
-
// ── ECC Savings banner ────────────────────────────────────────────────────────
|
|
1145
|
-
function renderSavings(s) {
|
|
1146
|
-
const at = s.allTime;
|
|
1147
|
-
if (!at.cacheSaved || at.cacheSaved < 0.001) return; // nothing to show yet
|
|
1148
|
-
|
|
1149
|
-
const costWithoutEcc = at.cost + at.cacheSaved;
|
|
1150
|
-
const savingsPct = costWithoutEcc > 0
|
|
1151
|
-
? Math.round((at.cacheSaved / costWithoutEcc) * 100) : 0;
|
|
1152
|
-
|
|
1153
|
-
document.getElementById('savingsBanner').style.display = 'block';
|
|
1154
|
-
setEl('sv-without', '$' + costWithoutEcc.toFixed(2));
|
|
1155
|
-
setEl('sv-actual', '$' + at.cost.toFixed(2));
|
|
1156
|
-
setEl('sv-saved', '$' + at.cacheSaved.toFixed(2));
|
|
1157
|
-
setEl('sv-pct', savingsPct + '% reduction');
|
|
1158
|
-
document.getElementById('sv-bar').style.width = Math.min(savingsPct, 100) + '%';
|
|
1159
|
-
setEl('sv-bar-paid', '$' + at.cost.toFixed(2) + ' paid');
|
|
1160
|
-
setEl('sv-bar-rate', at.cacheHitRate + '% cache hit rate');
|
|
1161
|
-
setEl('sv-bar-saved', '$' + at.cacheSaved.toFixed(2) + ' saved');
|
|
1162
|
-
}
|
|
1163
|
-
|
|
1164
|
-
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
1165
|
-
function setEl(id, val) {
|
|
1166
|
-
const el = document.getElementById(id);
|
|
1167
|
-
if (el) el.textContent = val;
|
|
1168
|
-
}
|
|
1169
|
-
|
|
1170
|
-
function fmtNum(n) {
|
|
1171
|
-
if (!n) return '0';
|
|
1172
|
-
return n >= 1000000 ? (n / 1000000).toFixed(1) + 'M'
|
|
1173
|
-
: n >= 1000 ? (n / 1000).toFixed(1) + 'K'
|
|
1174
|
-
: String(n);
|
|
1175
|
-
}
|
|
1176
|
-
|
|
1177
|
-
function fmtTokens(n) {
|
|
1178
|
-
if (!n) return '0';
|
|
1179
|
-
return n >= 1000000 ? (n / 1000000).toFixed(2) + 'M tok'
|
|
1180
|
-
: n >= 1000 ? (n / 1000).toFixed(1) + 'K tok'
|
|
1181
|
-
: n + ' tok';
|
|
1182
|
-
}
|
|
1183
|
-
|
|
1184
|
-
function fmtDate(d) {
|
|
1185
|
-
if (!d) return '—';
|
|
1186
|
-
const dt = new Date(d);
|
|
1187
|
-
const now = new Date();
|
|
1188
|
-
if (dt.toDateString() === now.toDateString()) return 'Today';
|
|
1189
|
-
const diff = Math.round((now - dt) / 86400000);
|
|
1190
|
-
if (diff === 1) return 'Yesterday';
|
|
1191
|
-
if (diff < 7) return `${diff}d ago`;
|
|
1192
|
-
return dt.toLocaleDateString(undefined, { month: 'short', day: 'numeric' });
|
|
1193
|
-
}
|
|
1194
|
-
|
|
1195
|
-
// Agent name → consistent color (hash)
|
|
1196
|
-
const _colorCache = {};
|
|
1197
|
-
function hexLight(name) {
|
|
1198
|
-
if (_colorCache[name]) return _colorCache[name];
|
|
1199
|
-
const PALETTE = ['#7c3aed','#2563eb','#059669','#d97706','#dc2626','#0891b2','#4f46e5','#e11d48'];
|
|
1200
|
-
let h = 0; for (const c of (name||'')) h = (h * 31 + c.charCodeAt(0)) & 0xffffffff;
|
|
1201
|
-
_colorCache[name] = PALETTE[Math.abs(h) % PALETTE.length];
|
|
1202
|
-
return _colorCache[name];
|
|
1203
|
-
}
|
|
1204
|
-
|
|
1205
|
-
// ── Auto-refresh every 60s ────────────────────────────────────────────────────
|
|
1206
|
-
function scheduleRefresh() {
|
|
1207
|
-
clearTimeout(_refreshTimer);
|
|
1208
|
-
_refreshTimer = setTimeout(() => { refreshData(); scheduleRefresh(); }, 60_000);
|
|
1209
|
-
}
|
|
1210
|
-
|
|
1211
|
-
// ── Boot ──────────────────────────────────────────────────────────────────────
|
|
1212
|
-
refreshData();
|
|
1213
|
-
scheduleRefresh();
|
|
1214
|
-
|
|
1215
|
-
// Redraw chart on resize
|
|
1216
|
-
window.addEventListener('resize', () => {
|
|
1217
|
-
if (_data?.costs) renderCostChart(_data.costs);
|
|
1218
|
-
});
|
|
1219
|
-
</script>
|
|
1220
|
-
</body>
|
|
1221
|
-
</html>
|