great-cto 2.76.0 → 2.77.1
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/board/.claude-plugin/plugin.json +256 -0
- package/board/packages/board/lib/alerts.mjs +413 -0
- package/board/packages/board/lib/beads.mjs +233 -0
- package/board/packages/board/lib/config.mjs +45 -0
- package/board/packages/board/lib/data-readers.mjs +340 -0
- package/board/packages/board/lib/fleet.mjs +363 -0
- package/board/packages/board/lib/metrics.mjs +282 -0
- package/board/packages/board/lib/notifications.mjs +50 -0
- package/board/packages/board/lib/projects.mjs +256 -0
- package/board/packages/board/lib/routes.mjs +1036 -0
- package/board/packages/board/lib/share.mjs +143 -0
- package/board/packages/board/lib/sse.mjs +20 -0
- package/board/packages/board/lib/state.mjs +31 -0
- package/board/packages/board/lib/util.mjs +36 -0
- package/board/packages/board/lib/verdicts.mjs +168 -0
- package/board/packages/board/lib/watchers.mjs +87 -0
- package/board/packages/board/mcp-server.mjs +310 -0
- package/board/packages/board/public/assets/apple-touch-icon.png +0 -0
- package/board/packages/board/public/assets/favicon-16.png +0 -0
- package/board/packages/board/public/assets/favicon-32.png +0 -0
- package/board/packages/board/public/assets/favicon.ico +0 -0
- package/board/packages/board/public/assets/favicon.svg +8 -0
- package/board/packages/board/public/index.html +5452 -0
- package/board/packages/board/public/share.html +1190 -0
- package/board/packages/board/public/sw.js +79 -0
- package/board/packages/board/push-adapter.mjs +222 -0
- package/board/packages/board/server.mjs +133 -0
- package/board/packages/cli/dist/archetypes.js +1461 -0
- package/board/scripts/lib/change-tier.mjs +119 -0
- package/board/scripts/lib/gate-plan.mjs +119 -0
- package/board/scripts/lib/judge-model.mjs +42 -0
- package/dist/board-path.js +47 -0
- package/dist/main.js +3 -31
- package/package.json +3 -1
|
@@ -0,0 +1,1190 @@
|
|
|
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
|
+
<meta name="robots" content="noindex, nofollow" />
|
|
7
|
+
<title>{{PROJECT}} · Engineering Report · {{DATE}}</title>
|
|
8
|
+
<meta property="og:title" content="{{PROJECT}} · Engineering Report" />
|
|
9
|
+
<meta property="og:description" content="AI agents shipped features, saved time and cost. See the breakdown." />
|
|
10
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
11
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
12
|
+
<link href="https://fonts.googleapis.com/css2?family=Geist:wght@400;500;600;700&family=Geist+Mono:wght@400;500&family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
|
|
13
|
+
<style>
|
|
14
|
+
:root {
|
|
15
|
+
/* Dark emerald — matches landing greatcto.systems */
|
|
16
|
+
--bg-page: #0a0e0c;
|
|
17
|
+
--bg-card: #11161a;
|
|
18
|
+
--bg-muted: #161c1f;
|
|
19
|
+
--bg-strong: #1e272c;
|
|
20
|
+
--bg-elevated: #1a2025;
|
|
21
|
+
--border: rgba(255, 255, 255, 0.06);
|
|
22
|
+
--border-strong: rgba(255, 255, 255, 0.12);
|
|
23
|
+
--text: #ecf2ee;
|
|
24
|
+
--text2: #8a9a92;
|
|
25
|
+
--text3: #4f5d57;
|
|
26
|
+
--accent: #00d97e;
|
|
27
|
+
--accent-soft: rgba(0, 217, 126, 0.10);
|
|
28
|
+
--accent-strong: rgba(0, 217, 126, 0.18);
|
|
29
|
+
--status-review: #00d97e;
|
|
30
|
+
--status-progress: #f59e0b;
|
|
31
|
+
--status-blocked: #ff5466;
|
|
32
|
+
--serif: "Geist", "Space Grotesk", "Inter", -apple-system, sans-serif;
|
|
33
|
+
--sans: "Geist", "Inter", -apple-system, BlinkMacSystemFont, sans-serif;
|
|
34
|
+
--mono: "Geist Mono", "JetBrains Mono", ui-monospace, "SF Mono", Menlo, monospace;
|
|
35
|
+
}
|
|
36
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
37
|
+
html, body {
|
|
38
|
+
width: 100%; min-height: 100%;
|
|
39
|
+
font-family: var(--sans); color: var(--text);
|
|
40
|
+
font-size: 14px; line-height: 1.5;
|
|
41
|
+
background: var(--bg-page);
|
|
42
|
+
-webkit-font-smoothing: antialiased;
|
|
43
|
+
}
|
|
44
|
+
button { font-family: inherit; cursor: pointer; }
|
|
45
|
+
|
|
46
|
+
/* Page wrap — compact */
|
|
47
|
+
.report-page {
|
|
48
|
+
position: relative;
|
|
49
|
+
width: 100%; min-height: 100vh;
|
|
50
|
+
display: flex; align-items: flex-start; justify-content: center;
|
|
51
|
+
padding: 32px 20px;
|
|
52
|
+
overflow: hidden;
|
|
53
|
+
}
|
|
54
|
+
.report-bg {
|
|
55
|
+
position: absolute; inset: 0; z-index: 0;
|
|
56
|
+
pointer-events: none;
|
|
57
|
+
background:
|
|
58
|
+
radial-gradient(ellipse 60% 50% at 50% 0%, rgba(0, 217, 126, 0.10), transparent 60%),
|
|
59
|
+
radial-gradient(ellipse 50% 40% at 100% 100%, rgba(0, 217, 126, 0.05), transparent 70%),
|
|
60
|
+
linear-gradient(180deg, #050807 0%, var(--bg-page) 100%);
|
|
61
|
+
}
|
|
62
|
+
.report-bg::after {
|
|
63
|
+
content: ""; position: absolute; inset: 0;
|
|
64
|
+
background-image:
|
|
65
|
+
linear-gradient(rgba(255,255,255,0.018) 1px, transparent 1px),
|
|
66
|
+
linear-gradient(90deg, rgba(255,255,255,0.018) 1px, transparent 1px);
|
|
67
|
+
background-size: 56px 56px;
|
|
68
|
+
mask-image: radial-gradient(ellipse 60% 50% at 50% 30%, #000 30%, transparent 80%);
|
|
69
|
+
-webkit-mask-image: radial-gradient(ellipse 60% 50% at 50% 30%, #000 30%, transparent 80%);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.report-card {
|
|
73
|
+
position: relative; z-index: 1;
|
|
74
|
+
width: 100%; max-width: 680px;
|
|
75
|
+
background: rgba(17, 22, 26, 0.92);
|
|
76
|
+
backdrop-filter: blur(20px);
|
|
77
|
+
-webkit-backdrop-filter: blur(20px);
|
|
78
|
+
border-radius: 16px;
|
|
79
|
+
box-shadow: 0 16px 48px rgba(15, 17, 21, 0.15), 0 1px 3px rgba(15, 17, 21, 0.06);
|
|
80
|
+
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
81
|
+
padding: 28px 32px;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.report-head {
|
|
85
|
+
display: flex; align-items: center; justify-content: space-between;
|
|
86
|
+
margin-bottom: 20px;
|
|
87
|
+
padding-bottom: 14px;
|
|
88
|
+
border-bottom: 1px solid var(--border);
|
|
89
|
+
}
|
|
90
|
+
.report-brand {
|
|
91
|
+
display: flex; align-items: center; gap: 9px;
|
|
92
|
+
font-family: var(--serif); font-size: 18px; font-weight: 600;
|
|
93
|
+
color: var(--text); letter-spacing: -0.01em;
|
|
94
|
+
}
|
|
95
|
+
.report-brand .brand-mark {
|
|
96
|
+
width: 22px; height: 22px;
|
|
97
|
+
border-radius: 5px;
|
|
98
|
+
background: var(--bg-page);
|
|
99
|
+
border: 1px solid var(--border-strong);
|
|
100
|
+
display: inline-flex; align-items: center; justify-content: center;
|
|
101
|
+
}
|
|
102
|
+
.report-brand .brand-mark svg { width: 18px; height: 18px; }
|
|
103
|
+
.report-status {
|
|
104
|
+
display: inline-flex; align-items: center; gap: 6px;
|
|
105
|
+
font-size: 10px; font-family: var(--mono);
|
|
106
|
+
color: var(--status-review);
|
|
107
|
+
background: rgba(22, 163, 74, 0.10);
|
|
108
|
+
padding: 4px 9px; border-radius: 999px;
|
|
109
|
+
letter-spacing: 0.04em;
|
|
110
|
+
}
|
|
111
|
+
.report-status .dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
|
|
112
|
+
|
|
113
|
+
.report-title { display: flex; gap: 14px; margin-bottom: 24px; }
|
|
114
|
+
.report-title .stripe {
|
|
115
|
+
width: 3px; background: var(--accent);
|
|
116
|
+
border-radius: 2px; flex-shrink: 0;
|
|
117
|
+
}
|
|
118
|
+
.report-title h1 {
|
|
119
|
+
font-family: var(--serif); font-weight: 600;
|
|
120
|
+
font-size: 28px; line-height: 1.15;
|
|
121
|
+
letter-spacing: -0.02em; margin: 0;
|
|
122
|
+
color: var(--text);
|
|
123
|
+
}
|
|
124
|
+
.report-title h1 em { font-style: normal; color: var(--accent); font-weight: 500; }
|
|
125
|
+
.report-title .meta {
|
|
126
|
+
font-family: var(--mono); font-size: 11px;
|
|
127
|
+
color: var(--text3); margin-top: 6px;
|
|
128
|
+
letter-spacing: 0.04em;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.section-label {
|
|
132
|
+
font-family: var(--mono); font-size: 10px;
|
|
133
|
+
letter-spacing: 0.1em; text-transform: uppercase;
|
|
134
|
+
color: var(--text3); margin: 18px 0 10px;
|
|
135
|
+
display: flex; align-items: center; gap: 8px;
|
|
136
|
+
}
|
|
137
|
+
.section-label.warn { color: #fcd34d; }
|
|
138
|
+
.section-label.warn svg { color: #fcd34d; }
|
|
139
|
+
|
|
140
|
+
.headline-narrative {
|
|
141
|
+
font-family: var(--serif); font-weight: 500;
|
|
142
|
+
font-size: 18px; line-height: 1.5;
|
|
143
|
+
color: var(--text);
|
|
144
|
+
letter-spacing: -0.01em;
|
|
145
|
+
margin: 18px 0 22px;
|
|
146
|
+
padding: 16px 18px;
|
|
147
|
+
background: linear-gradient(135deg, rgba(0,217,126,0.05), transparent 80%);
|
|
148
|
+
border-left: 2px solid var(--accent);
|
|
149
|
+
border-radius: 0 8px 8px 0;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.report-stats-hero {
|
|
153
|
+
display: grid; grid-template-columns: repeat(3, 1fr);
|
|
154
|
+
gap: 8px; margin-bottom: 8px;
|
|
155
|
+
}
|
|
156
|
+
.report-stat {
|
|
157
|
+
background: var(--bg-elevated); border: 1px solid var(--border);
|
|
158
|
+
border-radius: 8px; padding: 14px 14px;
|
|
159
|
+
position: relative; overflow: hidden;
|
|
160
|
+
}
|
|
161
|
+
.report-stat::before {
|
|
162
|
+
content: ""; position: absolute;
|
|
163
|
+
top: 0; left: 0; right: 0; height: 2px;
|
|
164
|
+
background: var(--accent);
|
|
165
|
+
}
|
|
166
|
+
.report-stat.green::before { background: var(--status-review); }
|
|
167
|
+
.report-stat.amber::before { background: var(--status-progress); }
|
|
168
|
+
.report-stat.red::before { background: var(--status-blocked); }
|
|
169
|
+
.report-stat.purple::before { background: #7c3aed; }
|
|
170
|
+
.report-stat .num {
|
|
171
|
+
font-family: var(--serif); font-weight: 500;
|
|
172
|
+
font-size: 32px; line-height: 1;
|
|
173
|
+
letter-spacing: -0.02em;
|
|
174
|
+
color: var(--text);
|
|
175
|
+
}
|
|
176
|
+
.report-stat .num small {
|
|
177
|
+
font-size: 14px; color: var(--text2);
|
|
178
|
+
margin-left: 2px; font-weight: 400;
|
|
179
|
+
}
|
|
180
|
+
.report-stat .lbl {
|
|
181
|
+
font-family: var(--mono); font-size: 10px;
|
|
182
|
+
letter-spacing: 0.08em; text-transform: uppercase;
|
|
183
|
+
color: var(--text2); margin-top: 10px;
|
|
184
|
+
}
|
|
185
|
+
.report-stat .sub {
|
|
186
|
+
margin-top: 4px; font-size: 11px; color: var(--text2);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
.report-stats-sec {
|
|
190
|
+
display: grid; grid-template-columns: repeat(3, 1fr);
|
|
191
|
+
gap: 8px; margin-bottom: 18px;
|
|
192
|
+
}
|
|
193
|
+
.report-stats-sec .report-stat .num { font-size: 22px; font-family: var(--serif); font-weight: 500; }
|
|
194
|
+
|
|
195
|
+
/* Compare card — AI vs humans */
|
|
196
|
+
.compare-card {
|
|
197
|
+
background: linear-gradient(135deg, rgba(0,217,126,0.08), rgba(167,139,250,0.04));
|
|
198
|
+
border: 1px solid var(--accent-strong);
|
|
199
|
+
border-radius: 10px;
|
|
200
|
+
padding: 14px 16px; margin-bottom: 18px;
|
|
201
|
+
display: grid; grid-template-columns: 1fr auto 1fr; gap: 14px;
|
|
202
|
+
align-items: center;
|
|
203
|
+
}
|
|
204
|
+
.compare-side { display: flex; flex-direction: column; gap: 2px; }
|
|
205
|
+
.compare-side .lbl {
|
|
206
|
+
font-family: var(--mono); font-size: 9px; letter-spacing: 0.1em;
|
|
207
|
+
text-transform: uppercase; color: var(--text2);
|
|
208
|
+
}
|
|
209
|
+
.compare-side .v {
|
|
210
|
+
font-family: var(--serif); font-weight: 500;
|
|
211
|
+
font-size: 26px; letter-spacing: -0.02em;
|
|
212
|
+
color: var(--text);
|
|
213
|
+
}
|
|
214
|
+
.compare-side .t { font-family: var(--mono); font-size: 11px; color: var(--text3); }
|
|
215
|
+
.compare-side.right { text-align: right; align-items: flex-end; }
|
|
216
|
+
.compare-vs {
|
|
217
|
+
font-family: var(--mono); font-size: 11px;
|
|
218
|
+
color: var(--text3); letter-spacing: 0.06em;
|
|
219
|
+
}
|
|
220
|
+
.compare-vs strong {
|
|
221
|
+
display: block; font-family: var(--serif);
|
|
222
|
+
font-size: 16px; color: var(--accent); font-weight: 600;
|
|
223
|
+
letter-spacing: -0.01em;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
.shipped-row {
|
|
227
|
+
display: grid;
|
|
228
|
+
grid-template-columns: 14px 1fr auto;
|
|
229
|
+
gap: 10px; align-items: baseline;
|
|
230
|
+
padding: 8px 0;
|
|
231
|
+
border-top: 1px solid var(--border);
|
|
232
|
+
font-size: 13px;
|
|
233
|
+
}
|
|
234
|
+
.shipped-row:first-of-type { border-top: none; }
|
|
235
|
+
|
|
236
|
+
/* Risk surface */
|
|
237
|
+
.risk-list { margin-bottom: 10px; }
|
|
238
|
+
.risk-row {
|
|
239
|
+
display: grid;
|
|
240
|
+
grid-template-columns: 90px 1fr auto;
|
|
241
|
+
gap: 10px; align-items: center;
|
|
242
|
+
padding: 10px 14px;
|
|
243
|
+
background: rgba(252, 211, 77, 0.04);
|
|
244
|
+
border: 1px solid rgba(252, 211, 77, 0.18);
|
|
245
|
+
border-radius: 8px;
|
|
246
|
+
margin-bottom: 6px;
|
|
247
|
+
font-size: 13px;
|
|
248
|
+
}
|
|
249
|
+
.risk-row.blocked { background: rgba(255, 84, 102, 0.04); border-color: rgba(255, 84, 102, 0.18); }
|
|
250
|
+
.risk-row.p0 { background: rgba(255, 84, 102, 0.06); border-color: rgba(255, 84, 102, 0.22); }
|
|
251
|
+
.risk-tag {
|
|
252
|
+
font-family: var(--mono); font-size: 10px; font-weight: 600;
|
|
253
|
+
letter-spacing: 0.06em; text-align: center;
|
|
254
|
+
padding: 3px 6px; border-radius: 4px;
|
|
255
|
+
background: rgba(252, 211, 77, 0.14); color: #fcd34d;
|
|
256
|
+
}
|
|
257
|
+
.risk-row.blocked .risk-tag,
|
|
258
|
+
.risk-row.p0 .risk-tag { background: rgba(255, 84, 102, 0.18); color: #fca5a5; }
|
|
259
|
+
.risk-ttl { color: var(--text); font-weight: 500; line-height: 1.35; }
|
|
260
|
+
.risk-meta { font-family: var(--mono); font-size: 10px; color: var(--text3); }
|
|
261
|
+
|
|
262
|
+
/* Coming up */
|
|
263
|
+
.coming-list { margin-bottom: 6px; }
|
|
264
|
+
.coming-row {
|
|
265
|
+
display: grid;
|
|
266
|
+
grid-template-columns: 16px 60px 1fr auto;
|
|
267
|
+
gap: 10px; align-items: center;
|
|
268
|
+
padding: 8px 0;
|
|
269
|
+
border-top: 1px solid var(--border);
|
|
270
|
+
font-size: 13px;
|
|
271
|
+
}
|
|
272
|
+
.coming-row:first-of-type { border-top: none; }
|
|
273
|
+
.coming-arrow { color: var(--accent); font-weight: 500; }
|
|
274
|
+
.coming-ttl { color: var(--text); }
|
|
275
|
+
.coming-agent {
|
|
276
|
+
font-family: var(--mono); font-size: 10.5px;
|
|
277
|
+
color: var(--text2);
|
|
278
|
+
background: rgba(0, 217, 126, 0.06);
|
|
279
|
+
padding: 2px 7px; border-radius: 4px;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/* Priority badges */
|
|
283
|
+
.badge {
|
|
284
|
+
font-family: var(--mono); font-size: 9.5px;
|
|
285
|
+
letter-spacing: 0.04em; font-weight: 500;
|
|
286
|
+
padding: 2px 7px; border-radius: 4px;
|
|
287
|
+
text-align: center;
|
|
288
|
+
}
|
|
289
|
+
.pri-p0 { background: rgba(255, 84, 102, 0.14); color: #fca5a5; }
|
|
290
|
+
.pri-p1 { background: rgba(255, 170, 60, 0.14); color: #fcd34d; }
|
|
291
|
+
.pri-p2 { background: rgba(240, 210, 78, 0.14); color: #fde68a; }
|
|
292
|
+
.pri-p3 { background: rgba(255, 255, 255, 0.05); color: #9ca3af; }
|
|
293
|
+
|
|
294
|
+
/* Shipped categories */
|
|
295
|
+
.ship-cat { margin-bottom: 8px; }
|
|
296
|
+
.ship-cat-h {
|
|
297
|
+
display: flex; align-items: center; gap: 8px;
|
|
298
|
+
padding: 10px 0 6px;
|
|
299
|
+
font-family: var(--mono); font-size: 10px;
|
|
300
|
+
letter-spacing: 0.1em;
|
|
301
|
+
border-top: 1px solid var(--border);
|
|
302
|
+
}
|
|
303
|
+
.ship-cat:first-of-type .ship-cat-h { border-top: none; padding-top: 4px; }
|
|
304
|
+
.ship-cat-dot {
|
|
305
|
+
width: 6px; height: 6px; border-radius: 50%;
|
|
306
|
+
}
|
|
307
|
+
.ship-cat-dot.security { background: #fca5a5; }
|
|
308
|
+
.ship-cat-dot.infra { background: #93c5fd; }
|
|
309
|
+
.ship-cat-dot.feature { background: var(--accent); }
|
|
310
|
+
.ship-cat-dot.docs { background: #c4b5fd; }
|
|
311
|
+
.ship-cat-name { color: var(--text2); }
|
|
312
|
+
.ship-cat-count { font-family: var(--mono); font-size: 10px; color: var(--text3); margin-left: auto; }
|
|
313
|
+
|
|
314
|
+
/* Agent breakdown */
|
|
315
|
+
.agent-list { margin-bottom: 8px; }
|
|
316
|
+
.agent-header-row {
|
|
317
|
+
display: grid;
|
|
318
|
+
grid-template-columns: 110px 1fr 72px 82px 52px;
|
|
319
|
+
gap: 10px; align-items: center;
|
|
320
|
+
padding: 4px 0 6px;
|
|
321
|
+
font-size: 10px; font-family: var(--mono);
|
|
322
|
+
letter-spacing: 0.08em; color: var(--text3);
|
|
323
|
+
border-bottom: 1px solid var(--border);
|
|
324
|
+
text-transform: uppercase;
|
|
325
|
+
}
|
|
326
|
+
.agent-header-row .agent-llm-h,
|
|
327
|
+
.agent-header-row .agent-human-h,
|
|
328
|
+
.agent-header-row .agent-tasks-h { text-align: right; }
|
|
329
|
+
.agent-row {
|
|
330
|
+
display: grid;
|
|
331
|
+
grid-template-columns: 110px 1fr 80px;
|
|
332
|
+
gap: 12px; align-items: center;
|
|
333
|
+
padding: 6px 0;
|
|
334
|
+
font-size: 12px;
|
|
335
|
+
}
|
|
336
|
+
.agent-row.rich {
|
|
337
|
+
grid-template-columns: 110px 1fr 72px 82px 52px;
|
|
338
|
+
gap: 10px;
|
|
339
|
+
}
|
|
340
|
+
.agent-row .agent-name { font-family: var(--mono); color: var(--text); }
|
|
341
|
+
.agent-row .agent-rail {
|
|
342
|
+
height: 4px; background: rgba(255,255,255,0.05);
|
|
343
|
+
border-radius: 2px; overflow: hidden;
|
|
344
|
+
}
|
|
345
|
+
.agent-row .agent-fill {
|
|
346
|
+
height: 100%; background: var(--accent);
|
|
347
|
+
}
|
|
348
|
+
.agent-row .agent-count {
|
|
349
|
+
font-family: var(--mono); font-size: 10.5px;
|
|
350
|
+
color: var(--text-2); text-align: right;
|
|
351
|
+
}
|
|
352
|
+
.agent-row.rich .agent-time {
|
|
353
|
+
display: flex; align-items: center; gap: 8px;
|
|
354
|
+
}
|
|
355
|
+
.agent-row.rich .agent-rail { flex: 1; }
|
|
356
|
+
.agent-time-v { font-family: var(--mono); font-size: 11px; color: var(--text2); white-space: nowrap; }
|
|
357
|
+
.agent-llm, .agent-human, .agent-tasks {
|
|
358
|
+
font-family: var(--mono); font-size: 11px;
|
|
359
|
+
text-align: right; color: var(--text2);
|
|
360
|
+
}
|
|
361
|
+
.agent-llm { color: #a78bfa; }
|
|
362
|
+
.agent-human { color: #fb923c; }
|
|
363
|
+
.agent-total-row {
|
|
364
|
+
display: grid;
|
|
365
|
+
grid-template-columns: 110px 1fr 72px 82px 52px;
|
|
366
|
+
gap: 10px; align-items: center;
|
|
367
|
+
padding: 6px 0 4px;
|
|
368
|
+
border-top: 1px solid var(--border);
|
|
369
|
+
margin-top: 4px;
|
|
370
|
+
font-size: 12px; font-weight: 600;
|
|
371
|
+
}
|
|
372
|
+
.agent-total-row .agent-name { font-family: var(--mono); color: var(--text); }
|
|
373
|
+
.agent-total-row .agent-time { display: flex; }
|
|
374
|
+
.agent-total-row .agent-llm,
|
|
375
|
+
.agent-total-row .agent-human,
|
|
376
|
+
.agent-total-row .agent-tasks { font-family: var(--mono); font-size: 11px; text-align: right; font-weight: 600; }
|
|
377
|
+
.agent-total-row .agent-llm { color: #a78bfa; }
|
|
378
|
+
.agent-total-row .agent-human { color: #fb923c; }
|
|
379
|
+
|
|
380
|
+
/* Trust footer */
|
|
381
|
+
.trust-foot {
|
|
382
|
+
margin-top: 22px;
|
|
383
|
+
padding: 16px 18px;
|
|
384
|
+
background: rgba(255, 255, 255, 0.015);
|
|
385
|
+
border: 1px solid var(--border);
|
|
386
|
+
border-radius: 10px;
|
|
387
|
+
font-family: var(--mono); font-size: 10.5px;
|
|
388
|
+
line-height: 1.6;
|
|
389
|
+
}
|
|
390
|
+
.trust-row {
|
|
391
|
+
display: grid; grid-template-columns: 80px 1fr;
|
|
392
|
+
gap: 12px; align-items: baseline;
|
|
393
|
+
padding: 4px 0;
|
|
394
|
+
color: var(--text-2);
|
|
395
|
+
}
|
|
396
|
+
.trust-row + .trust-row { border-top: 1px solid var(--border); padding-top: 8px; margin-top: 4px; }
|
|
397
|
+
.trust-k { color: var(--text-3); letter-spacing: 0.04em; text-transform: uppercase; font-size: 9.5px; }
|
|
398
|
+
.trust-v code {
|
|
399
|
+
background: rgba(255,255,255,0.04);
|
|
400
|
+
padding: 1px 5px; border-radius: 3px;
|
|
401
|
+
color: var(--accent);
|
|
402
|
+
border: 1px solid var(--border);
|
|
403
|
+
}
|
|
404
|
+
.trust-v a { color: var(--accent); text-decoration: none; }
|
|
405
|
+
.trust-v a:hover { text-decoration: underline; }
|
|
406
|
+
|
|
407
|
+
/* Compare card — symmetrical time + cost */
|
|
408
|
+
.compare-card .compare-vs { text-align: center; }
|
|
409
|
+
.compare-card .compare-vs .vs-line {
|
|
410
|
+
font-family: var(--mono); font-size: 9.5px;
|
|
411
|
+
color: var(--text-3); letter-spacing: 0.06em;
|
|
412
|
+
}
|
|
413
|
+
.compare-card .compare-vs strong {
|
|
414
|
+
display: block; margin: 2px 0;
|
|
415
|
+
font-family: var(--serif); font-size: 16px;
|
|
416
|
+
font-weight: 600; color: var(--accent);
|
|
417
|
+
letter-spacing: -0.01em;
|
|
418
|
+
white-space: nowrap;
|
|
419
|
+
}
|
|
420
|
+
.compare-card .compare-vs .vs-time {
|
|
421
|
+
font-family: var(--mono); font-size: 10px;
|
|
422
|
+
color: var(--text2);
|
|
423
|
+
}
|
|
424
|
+
.shipped-check {
|
|
425
|
+
width: 14px; height: 14px;
|
|
426
|
+
border-radius: 50%;
|
|
427
|
+
background: var(--status-review); color: #fff;
|
|
428
|
+
display: inline-flex; align-items: center; justify-content: center;
|
|
429
|
+
font-size: 9px; margin-top: 2px;
|
|
430
|
+
}
|
|
431
|
+
.shipped-row .ttl { color: var(--text); }
|
|
432
|
+
.shipped-row .ttl .sub {
|
|
433
|
+
color: var(--text3); display: block;
|
|
434
|
+
font-size: 11px; margin-top: 1px;
|
|
435
|
+
}
|
|
436
|
+
.shipped-row .when {
|
|
437
|
+
font-family: var(--mono); font-size: 10px; color: var(--text3);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
.report-foot {
|
|
441
|
+
margin-top: 22px;
|
|
442
|
+
padding-top: 14px;
|
|
443
|
+
border-top: 1px solid var(--border);
|
|
444
|
+
text-align: center;
|
|
445
|
+
font-family: var(--mono);
|
|
446
|
+
font-size: 10px; letter-spacing: 0.06em;
|
|
447
|
+
color: var(--text3);
|
|
448
|
+
}
|
|
449
|
+
.report-foot a { color: var(--accent); text-decoration: none; }
|
|
450
|
+
|
|
451
|
+
/* Paused */
|
|
452
|
+
.paused-card {
|
|
453
|
+
text-align: center;
|
|
454
|
+
padding: 64px 44px;
|
|
455
|
+
}
|
|
456
|
+
.paused-icon {
|
|
457
|
+
width: 64px; height: 64px;
|
|
458
|
+
margin: 0 auto 24px;
|
|
459
|
+
border-radius: 50%;
|
|
460
|
+
background: var(--bg-muted);
|
|
461
|
+
display: flex; align-items: center; justify-content: center;
|
|
462
|
+
color: var(--text2);
|
|
463
|
+
}
|
|
464
|
+
.paused-card h1 {
|
|
465
|
+
font-family: var(--serif); font-weight: 600;
|
|
466
|
+
font-size: 26px; letter-spacing: -0.01em;
|
|
467
|
+
margin: 0 0 10px;
|
|
468
|
+
}
|
|
469
|
+
.paused-card p {
|
|
470
|
+
color: var(--text2); margin: 0 auto 28px;
|
|
471
|
+
max-width: 40ch;
|
|
472
|
+
}
|
|
473
|
+
.btn-back {
|
|
474
|
+
background: var(--bg-elevated); color: var(--accent);
|
|
475
|
+
border: 1px solid var(--border-strong);
|
|
476
|
+
padding: 12px 24px; border-radius: 999px;
|
|
477
|
+
font-weight: 500; font-size: 14px;
|
|
478
|
+
text-decoration: none; display: inline-block;
|
|
479
|
+
}
|
|
480
|
+
.btn-back:hover { background: var(--bg-muted); }
|
|
481
|
+
|
|
482
|
+
/* ═══════ NEW VISUAL COMPONENTS ═══════════════════════════════════════════ */
|
|
483
|
+
|
|
484
|
+
/* 1) HERO BIG — large headline number with sparkline */
|
|
485
|
+
.hero-big {
|
|
486
|
+
background: linear-gradient(135deg, rgba(0,217,126,0.06), rgba(0,0,0,0));
|
|
487
|
+
border: 1px solid var(--accent-strong);
|
|
488
|
+
border-radius: 14px;
|
|
489
|
+
padding: 24px 24px 18px;
|
|
490
|
+
margin-bottom: 18px;
|
|
491
|
+
position: relative; overflow: hidden;
|
|
492
|
+
}
|
|
493
|
+
.hero-big::before {
|
|
494
|
+
content: ""; position: absolute; inset: 0;
|
|
495
|
+
background: radial-gradient(ellipse 80% 60% at 20% 0%, rgba(0,217,126,0.10), transparent 60%);
|
|
496
|
+
pointer-events: none;
|
|
497
|
+
}
|
|
498
|
+
.hero-big .hero-row {
|
|
499
|
+
display: flex; align-items: flex-end; gap: 24px;
|
|
500
|
+
position: relative; z-index: 1;
|
|
501
|
+
flex-wrap: wrap;
|
|
502
|
+
}
|
|
503
|
+
.hero-big .hero-left { flex: 0 0 auto; }
|
|
504
|
+
.hero-big .hero-num {
|
|
505
|
+
font-family: var(--serif); font-weight: 600;
|
|
506
|
+
font-size: 92px; line-height: 0.9;
|
|
507
|
+
letter-spacing: -0.04em;
|
|
508
|
+
color: var(--text);
|
|
509
|
+
background: linear-gradient(180deg, #ecf2ee 0%, #8a9a92 100%);
|
|
510
|
+
-webkit-background-clip: text; background-clip: text;
|
|
511
|
+
-webkit-text-fill-color: transparent;
|
|
512
|
+
}
|
|
513
|
+
.hero-big .hero-lbl {
|
|
514
|
+
font-family: var(--mono); font-size: 11px;
|
|
515
|
+
letter-spacing: 0.12em; text-transform: uppercase;
|
|
516
|
+
color: var(--text2); margin-top: 6px;
|
|
517
|
+
}
|
|
518
|
+
.hero-big .hero-sub {
|
|
519
|
+
font-size: 13px; color: var(--text2); margin-top: 8px;
|
|
520
|
+
}
|
|
521
|
+
.hero-big .hero-sub .delta-up { color: var(--status-review); font-weight: 600; }
|
|
522
|
+
.hero-big .hero-spark {
|
|
523
|
+
flex: 1 1 200px; min-width: 200px;
|
|
524
|
+
height: 56px; opacity: 0.9;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/* 2) COST BAR CHART — visual horizontal bars */
|
|
528
|
+
.cost-bars {
|
|
529
|
+
background: var(--bg-elevated);
|
|
530
|
+
border: 1px solid var(--border);
|
|
531
|
+
border-radius: 12px;
|
|
532
|
+
padding: 18px 20px;
|
|
533
|
+
margin-bottom: 18px;
|
|
534
|
+
}
|
|
535
|
+
.cost-bars .cost-headline {
|
|
536
|
+
text-align: center; margin-bottom: 14px;
|
|
537
|
+
}
|
|
538
|
+
.cost-bars .cost-headline .big {
|
|
539
|
+
font-family: var(--serif); font-weight: 600;
|
|
540
|
+
font-size: 36px; letter-spacing: -0.02em;
|
|
541
|
+
color: var(--accent);
|
|
542
|
+
line-height: 1;
|
|
543
|
+
}
|
|
544
|
+
.cost-bars .cost-headline .lbl {
|
|
545
|
+
font-family: var(--mono); font-size: 10px;
|
|
546
|
+
letter-spacing: 0.12em; text-transform: uppercase;
|
|
547
|
+
color: var(--text2); margin-top: 6px;
|
|
548
|
+
}
|
|
549
|
+
.cost-bar-row {
|
|
550
|
+
display: grid;
|
|
551
|
+
grid-template-columns: 78px 1fr 80px;
|
|
552
|
+
gap: 12px; align-items: center;
|
|
553
|
+
padding: 8px 0;
|
|
554
|
+
}
|
|
555
|
+
.cost-bar-row .cb-name {
|
|
556
|
+
font-family: var(--mono); font-size: 10px;
|
|
557
|
+
letter-spacing: 0.08em; text-transform: uppercase;
|
|
558
|
+
color: var(--text2);
|
|
559
|
+
}
|
|
560
|
+
.cost-bar-row .cb-rail {
|
|
561
|
+
height: 22px; background: rgba(255,255,255,0.04);
|
|
562
|
+
border-radius: 4px; overflow: hidden; position: relative;
|
|
563
|
+
}
|
|
564
|
+
.cost-bar-row .cb-fill {
|
|
565
|
+
height: 100%; border-radius: 4px;
|
|
566
|
+
transition: width .6s cubic-bezier(.2,.7,.3,1);
|
|
567
|
+
}
|
|
568
|
+
.cost-bar-row.human .cb-fill {
|
|
569
|
+
background: linear-gradient(90deg, #ff5466, #ff8c66);
|
|
570
|
+
}
|
|
571
|
+
.cost-bar-row.ai .cb-fill {
|
|
572
|
+
background: linear-gradient(90deg, #00d97e, #34d399);
|
|
573
|
+
}
|
|
574
|
+
.cost-bar-row .cb-val {
|
|
575
|
+
font-family: var(--mono); font-size: 13px;
|
|
576
|
+
color: var(--text); text-align: right;
|
|
577
|
+
font-weight: 500;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/* 3) ACTIVITY HEATMAP — GitHub-style 30-day grid */
|
|
581
|
+
.activity-heatmap {
|
|
582
|
+
background: var(--bg-elevated);
|
|
583
|
+
border: 1px solid var(--border);
|
|
584
|
+
border-radius: 12px;
|
|
585
|
+
padding: 16px 20px;
|
|
586
|
+
margin-bottom: 18px;
|
|
587
|
+
}
|
|
588
|
+
.activity-heatmap .ah-head {
|
|
589
|
+
display: flex; justify-content: space-between; align-items: center;
|
|
590
|
+
margin-bottom: 10px;
|
|
591
|
+
}
|
|
592
|
+
.activity-heatmap .ah-title {
|
|
593
|
+
font-family: var(--mono); font-size: 10px;
|
|
594
|
+
letter-spacing: 0.12em; text-transform: uppercase;
|
|
595
|
+
color: var(--text2);
|
|
596
|
+
}
|
|
597
|
+
.activity-heatmap .ah-legend {
|
|
598
|
+
display: flex; align-items: center; gap: 6px;
|
|
599
|
+
font-family: var(--mono); font-size: 9px;
|
|
600
|
+
color: var(--text3); letter-spacing: 0.06em;
|
|
601
|
+
}
|
|
602
|
+
.activity-heatmap .ah-legend-cells {
|
|
603
|
+
display: flex; gap: 2px;
|
|
604
|
+
}
|
|
605
|
+
.ah-cell {
|
|
606
|
+
width: 10px; height: 10px; border-radius: 2px;
|
|
607
|
+
background: rgba(255,255,255,0.04);
|
|
608
|
+
}
|
|
609
|
+
.ah-cell.l1 { background: rgba(0,217,126,0.18); }
|
|
610
|
+
.ah-cell.l2 { background: rgba(0,217,126,0.36); }
|
|
611
|
+
.ah-cell.l3 { background: rgba(0,217,126,0.58); }
|
|
612
|
+
.ah-cell.l4 { background: rgba(0,217,126,0.92); }
|
|
613
|
+
.ah-grid {
|
|
614
|
+
display: grid; grid-template-columns: repeat(30, 1fr);
|
|
615
|
+
gap: 3px;
|
|
616
|
+
}
|
|
617
|
+
.ah-grid .ah-cell {
|
|
618
|
+
width: 100%; aspect-ratio: 1 / 1; max-width: 16px;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/* 4) AGENT BUBBLES — bubble chart instead of table */
|
|
622
|
+
.agent-bubbles-wrap {
|
|
623
|
+
background: var(--bg-elevated);
|
|
624
|
+
border: 1px solid var(--border);
|
|
625
|
+
border-radius: 12px;
|
|
626
|
+
padding: 16px 16px 8px;
|
|
627
|
+
margin-bottom: 18px;
|
|
628
|
+
}
|
|
629
|
+
.agent-bubbles-wrap .ab-title {
|
|
630
|
+
font-family: var(--mono); font-size: 10px;
|
|
631
|
+
letter-spacing: 0.12em; text-transform: uppercase;
|
|
632
|
+
color: var(--text2); margin-bottom: 10px;
|
|
633
|
+
display: flex; justify-content: space-between;
|
|
634
|
+
}
|
|
635
|
+
.agent-bubbles-wrap .ab-svg { width: 100%; height: auto; display: block; }
|
|
636
|
+
.agent-bubbles-wrap .ab-bubble text { font-family: var(--mono); }
|
|
637
|
+
.agent-bubbles-wrap .ab-bubble-name { fill: #fff; font-weight: 500; }
|
|
638
|
+
.agent-bubbles-wrap .ab-bubble-meta { fill: rgba(255,255,255,0.7); }
|
|
639
|
+
.agent-bubbles-wrap .ab-foot {
|
|
640
|
+
display: flex; justify-content: space-between;
|
|
641
|
+
font-family: var(--mono); font-size: 10px;
|
|
642
|
+
color: var(--text3); padding: 8px 4px 0;
|
|
643
|
+
border-top: 1px solid var(--border);
|
|
644
|
+
margin-top: 8px;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
</style>
|
|
648
|
+
</head>
|
|
649
|
+
<body>
|
|
650
|
+
<div id="root"></div>
|
|
651
|
+
<script>
|
|
652
|
+
const metrics = {{METRICS_JSON}};
|
|
653
|
+
const tasks = {{TASKS_JSON}};
|
|
654
|
+
const project = '{{PROJECT}}';
|
|
655
|
+
const date = '{{DATE}}';
|
|
656
|
+
const paused = {{PAUSED}};
|
|
657
|
+
|
|
658
|
+
function esc(s) {
|
|
659
|
+
// BH-26: also escape " and ' — see index.html:esc.
|
|
660
|
+
return String(s || '')
|
|
661
|
+
.replace(/&/g, '&')
|
|
662
|
+
.replace(/</g, '<')
|
|
663
|
+
.replace(/>/g, '>')
|
|
664
|
+
.replace(/"/g, '"')
|
|
665
|
+
.replace(/'/g, ''');
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// Background uses pure CSS (radial-gradients + grid), no SVG needed.
|
|
669
|
+
const LANDSCAPE_SVG = '';
|
|
670
|
+
|
|
671
|
+
if (paused) {
|
|
672
|
+
document.getElementById('root').innerHTML = `
|
|
673
|
+
<div class="report-page">
|
|
674
|
+
<div class="report-bg">${LANDSCAPE_SVG}</div>
|
|
675
|
+
<div class="report-card paused-card">
|
|
676
|
+
<div class="paused-icon">
|
|
677
|
+
<svg width="28" height="28" viewBox="0 0 24 24" fill="currentColor"><path d="M6 4h4v16H6z M14 4h4v16h-4z"/></svg>
|
|
678
|
+
</div>
|
|
679
|
+
<h1>Report paused</h1>
|
|
680
|
+
<p>The owner has temporarily disabled this link. Reach out to them if you need updated metrics.</p>
|
|
681
|
+
<a class="btn-back" href="https://greatcto.systems">← Back to greatcto.systems</a>
|
|
682
|
+
<div class="report-foot" style="margin-top:40px;">
|
|
683
|
+
CREATED BY <a href="https://greatcto.systems">GREATCTO</a> · GREATCTO.SYSTEMS
|
|
684
|
+
</div>
|
|
685
|
+
</div>
|
|
686
|
+
</div>`;
|
|
687
|
+
} else {
|
|
688
|
+
const m = metrics || {};
|
|
689
|
+
const t_ = m.tasks || {};
|
|
690
|
+
// Share report uses LIFETIME counts — this is a marketing artifact, not an
|
|
691
|
+
// internal dashboard. Board UI has its own period selector (1D/7D/30D/...).
|
|
692
|
+
const done = t_.done ?? 0;
|
|
693
|
+
const inProgress = t_.in_progress ?? 0;
|
|
694
|
+
const backlog = t_.backlog ?? 0;
|
|
695
|
+
const allTasks = tasks || [];
|
|
696
|
+
const doneTasks = allTasks.filter(t => t.status === 'done' || t.status === 'closed');
|
|
697
|
+
const openTasks = allTasks.filter(t => t.status !== 'done' && t.status !== 'closed' && t.status !== 'gate');
|
|
698
|
+
|
|
699
|
+
function fmtTime(min) {
|
|
700
|
+
if (!min || min <= 0) return '—';
|
|
701
|
+
// BH-A5: sub-minute tasks rounded to "0m" — show seconds instead.
|
|
702
|
+
if (min < 1) return Math.max(1, Math.round(min * 60)) + 's';
|
|
703
|
+
if (min < 60) return Math.round(min) + 'm';
|
|
704
|
+
const h = min / 60;
|
|
705
|
+
if (h < 24) return h.toFixed(h < 10 ? 1 : 0) + 'h';
|
|
706
|
+
return (h / 8).toFixed(1) + 'd';
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
// AI active time = sum of per-agent estimates from server (uses
|
|
710
|
+
// estimated_minutes or 30min default per task). Server already excludes
|
|
711
|
+
// wall-clock noise — see getMetrics() in server.mjs.
|
|
712
|
+
const totalAiMin = (m.agents_cost || []).reduce((s, a) => s + (a.time_min || 0), 0);
|
|
713
|
+
|
|
714
|
+
const llmUsd = m.cost?.llm_usd ?? 0;
|
|
715
|
+
const humanUsd = m.cost?.human_usd ?? 0;
|
|
716
|
+
const savingsX = m.cost?.savings_x ?? 0;
|
|
717
|
+
// Human estimate: avg 4h per task (industry baseline for standard feature)
|
|
718
|
+
const humanH = done * 4;
|
|
719
|
+
const aiH = totalAiMin / 60;
|
|
720
|
+
// BH-A5: when AI time < 5 min total, the human/ai ratio explodes into
|
|
721
|
+
// 28,800× nonsense (sub-second tasks). Clamp + suppress to keep the page
|
|
722
|
+
// credible. Threshold = 5 min total AI work, or > 1000× ratio.
|
|
723
|
+
const rawSpeedup = aiH > 0 && humanH > 0 ? humanH / aiH : 0;
|
|
724
|
+
// "faster" only makes sense when AI is faster than the human estimate (ratio >= 1.5).
|
|
725
|
+
// Sub-1 ratios mean AI was slower — never label that as "faster".
|
|
726
|
+
const timeSpeedup = (totalAiMin >= 5 && rawSpeedup >= 1.5 && rawSpeedup < 1000)
|
|
727
|
+
? rawSpeedup.toFixed(rawSpeedup < 10 ? 1 : 0) : null;
|
|
728
|
+
|
|
729
|
+
// Categorize done tasks (heuristic from title prefix / labels / keywords)
|
|
730
|
+
function categorize(t) {
|
|
731
|
+
const ttl = (t.title || '').toLowerCase();
|
|
732
|
+
const labels = (t.labels || []).map(l => l.toLowerCase());
|
|
733
|
+
if (labels.includes('security') || /\b(sec|cve|auth|webhook signature|gdpr|csrf|injection)\b/.test(ttl)) return 'security';
|
|
734
|
+
if (labels.includes('infra') || /\b(infra|deploy|postgres|redis|s3|docker|k8s|telemetry|ci|backup|pool|replica|rate limit)\b/.test(ttl)) return 'infra';
|
|
735
|
+
if (labels.includes('docs') || /\b(docs?|readme|changelog)\b/.test(ttl)) return 'docs';
|
|
736
|
+
return 'feature';
|
|
737
|
+
}
|
|
738
|
+
const categories = { security: [], infra: [], feature: [], docs: [] };
|
|
739
|
+
doneTasks.forEach(t => categories[categorize(t)].push(t));
|
|
740
|
+
|
|
741
|
+
// Risk surface — open P0 + blocked + stale
|
|
742
|
+
const p0Open = openTasks.filter(t => t.priority === 0);
|
|
743
|
+
const blocked = openTasks.filter(t => t.status === 'blocked');
|
|
744
|
+
const stale = openTasks.filter(t => {
|
|
745
|
+
if (t.status !== 'in_progress' || !t.updated_at) return false;
|
|
746
|
+
const ageH = (Date.now() - new Date(t.updated_at).getTime()) / 3600_000;
|
|
747
|
+
return ageH > 48;
|
|
748
|
+
});
|
|
749
|
+
const riskItems = [...p0Open, ...blocked, ...stale.filter(t => !p0Open.includes(t) && !blocked.includes(t))].slice(0, 4);
|
|
750
|
+
|
|
751
|
+
// Coming up — top 3 backlog by priority
|
|
752
|
+
const coming = openTasks
|
|
753
|
+
.filter(t => t.status === 'backlog' || t.status === 'open')
|
|
754
|
+
.sort((a, b) => (a.priority ?? 3) - (b.priority ?? 3))
|
|
755
|
+
.slice(0, 3);
|
|
756
|
+
|
|
757
|
+
// Agent breakdown — from agents_cost (includes time + cost) or fall back to verdicts counts
|
|
758
|
+
const agentsCost = (m.agents_cost || []).filter(a => a.tasks_total > 0);
|
|
759
|
+
const agents = m.agents || {};
|
|
760
|
+
const totalAgentRuns = Object.values(agents).reduce((a, b) => a + b, 0);
|
|
761
|
+
const agentList = Object.entries(agents).sort((a, b) => b[1] - a[1]).slice(0, 6);
|
|
762
|
+
|
|
763
|
+
// Totals for compare card (use agents_cost data if available, else fallback to existing cost)
|
|
764
|
+
const totalLlmFromAgents = agentsCost.reduce((s, a) => s + a.llm_usd, 0);
|
|
765
|
+
const totalHumanFromAgents = agentsCost.reduce((s, a) => s + a.human_usd, 0);
|
|
766
|
+
const totalTimeMin = agentsCost.reduce((s, a) => s + a.time_min, 0);
|
|
767
|
+
// AI cost: prefer real LLM token cost (from verdicts/plans), fall back to estimate
|
|
768
|
+
const realLlmUsd = m.cost?.real_llm_usd ?? 0;
|
|
769
|
+
const effectiveLlmUsd = realLlmUsd > 0
|
|
770
|
+
? realLlmUsd
|
|
771
|
+
: (llmUsd > 0 ? llmUsd : totalLlmFromAgents);
|
|
772
|
+
// Human cost: industry baseline $150/hr × 4h per task = $600 per feature
|
|
773
|
+
const HUMAN_PER_TASK_USD = 4 * 150; // 4 hours × $150/hr
|
|
774
|
+
const effectiveHumanUsd = done > 0
|
|
775
|
+
? done * HUMAN_PER_TASK_USD
|
|
776
|
+
: (humanUsd > 0 ? humanUsd : totalHumanFromAgents);
|
|
777
|
+
// Savings ratio is trustworthy when:
|
|
778
|
+
// (a) AI cost = real LLM tokens (not derived from the same task-min base as human), OR
|
|
779
|
+
// (b) Cost source explicitly comes from plans/verdicts (independent measurement)
|
|
780
|
+
const savingsTrustworthy = realLlmUsd > 0 || (m.cost?.source && m.cost.source !== 'tasks');
|
|
781
|
+
const effectiveSavings = savingsTrustworthy && effectiveLlmUsd > 0 && effectiveHumanUsd > 0
|
|
782
|
+
? Math.round(effectiveHumanUsd / effectiveLlmUsd).toLocaleString() + '×' : null;
|
|
783
|
+
const effectiveAiH = totalAiMin > 0 ? totalAiMin / 60 : totalTimeMin / 60;
|
|
784
|
+
|
|
785
|
+
// Hero narrative — focus on cost difference, not time
|
|
786
|
+
const headline = (() => {
|
|
787
|
+
if (!done) return '';
|
|
788
|
+
const parts = [`${done} feature${done === 1 ? '' : 's'} shipped`];
|
|
789
|
+
// AI spend (prefer real LLM token cost from verdicts)
|
|
790
|
+
const realLlm = m.cost?.real_llm_usd ?? 0;
|
|
791
|
+
const aiCostDisplay = realLlm > 0
|
|
792
|
+
? `$${realLlm < 10 ? realLlm.toFixed(2) : realLlm.toFixed(0)}`
|
|
793
|
+
: (llmUsd > 0 ? `$${llmUsd < 10 ? llmUsd.toFixed(2) : llmUsd.toFixed(0)}` : null);
|
|
794
|
+
if (aiCostDisplay) parts.push(`for ${aiCostDisplay} in LLM tokens`);
|
|
795
|
+
let line = parts.join(' ');
|
|
796
|
+
// Human equivalent: standard 4h per feature × $150/hr
|
|
797
|
+
const humanCost = done * 4 * 150;
|
|
798
|
+
if (aiCostDisplay && humanCost > 0) {
|
|
799
|
+
line += `. Human team estimate: $${humanCost.toLocaleString()}.`;
|
|
800
|
+
}
|
|
801
|
+
return line;
|
|
802
|
+
})();
|
|
803
|
+
|
|
804
|
+
const hero = [
|
|
805
|
+
{
|
|
806
|
+
v: done || '—',
|
|
807
|
+
sub: done ? 'done' : '',
|
|
808
|
+
lbl: 'Tasks shipped',
|
|
809
|
+
extra: `+${m.velocity?.this_week ?? 0} this week`,
|
|
810
|
+
cls: 'green',
|
|
811
|
+
},
|
|
812
|
+
{
|
|
813
|
+
v: totalAiMin ? fmtTime(totalAiMin) : '—',
|
|
814
|
+
sub: '',
|
|
815
|
+
lbl: 'AI time',
|
|
816
|
+
extra: done && totalAiMin > 0 ? `${fmtTime(totalAiMin / Math.max(1, done))} avg / task` : '—',
|
|
817
|
+
cls: 'amber',
|
|
818
|
+
hide: !totalAiMin,
|
|
819
|
+
},
|
|
820
|
+
{
|
|
821
|
+
v: llmUsd > 0 ? '$' + llmUsd.toFixed(0) : '—',
|
|
822
|
+
sub: '',
|
|
823
|
+
lbl: 'LLM cost',
|
|
824
|
+
extra: m.cost?.count ? `${m.cost.count} plans` : 'no plans',
|
|
825
|
+
cls: 'purple',
|
|
826
|
+
hide: !llmUsd,
|
|
827
|
+
},
|
|
828
|
+
].filter(s => !s.hide);
|
|
829
|
+
|
|
830
|
+
const sec = [
|
|
831
|
+
{ v: m.qa?.pass_rate != null ? m.qa.pass_rate + '%' : '—', lbl: 'QA pass rate', extra: `${m.qa?.passed ?? 0} pass · ${m.qa?.failed ?? 0} fail`, cls: 'green' },
|
|
832
|
+
{ v: m.security?.approved ?? 0, lbl: 'Security approved', extra: m.security?.blocked ? `${m.security.blocked} blocked` : 'no blocks' },
|
|
833
|
+
{ v: inProgress, lbl: 'In progress', extra: `${backlog} in backlog` },
|
|
834
|
+
];
|
|
835
|
+
|
|
836
|
+
// Helpers for rendering rows
|
|
837
|
+
const PRI = ['Urgent', 'High', 'Medium', 'Low'];
|
|
838
|
+
function priChip(p) {
|
|
839
|
+
const cls = `pri-p${p ?? 3}`;
|
|
840
|
+
return `<span class="badge ${cls}">${PRI[p ?? 3]}</span>`;
|
|
841
|
+
}
|
|
842
|
+
function whenStr(iso) {
|
|
843
|
+
if (!iso) return '';
|
|
844
|
+
return new Date(iso).toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
|
|
845
|
+
}
|
|
846
|
+
function ageStr(iso) {
|
|
847
|
+
if (!iso) return '';
|
|
848
|
+
const h = (Date.now() - new Date(iso).getTime()) / 3600_000;
|
|
849
|
+
if (h < 1) return `${Math.round(h * 60)}m ago`;
|
|
850
|
+
if (h < 48) return `${Math.round(h)}h ago`;
|
|
851
|
+
return `${Math.round(h / 24)}d ago`;
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
const generatedAt = new Date().toLocaleString('en-US', { dateStyle: 'medium', timeStyle: 'short' });
|
|
855
|
+
|
|
856
|
+
// ─── Visual helpers ──────────────────────────────────────────────────────
|
|
857
|
+
// Bucket done tasks by day (last 30 days) using closed_at
|
|
858
|
+
function bucketByDay(tasks, days) {
|
|
859
|
+
const buckets = new Array(days).fill(0);
|
|
860
|
+
const now = Date.now();
|
|
861
|
+
const dayMs = 86400_000;
|
|
862
|
+
tasks.forEach(t => {
|
|
863
|
+
const iso = t.closed_at || t.updated_at;
|
|
864
|
+
if (!iso) return;
|
|
865
|
+
const age = Math.floor((now - new Date(iso).getTime()) / dayMs);
|
|
866
|
+
if (age >= 0 && age < days) buckets[days - 1 - age] += 1;
|
|
867
|
+
});
|
|
868
|
+
return buckets;
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
// SVG sparkline from numeric series
|
|
872
|
+
function sparklineSvg(values, w, h, color) {
|
|
873
|
+
if (!values.length) return '';
|
|
874
|
+
const max = Math.max(1, ...values);
|
|
875
|
+
const stepX = w / Math.max(1, values.length - 1);
|
|
876
|
+
const pts = values.map((v, i) => {
|
|
877
|
+
const x = i * stepX;
|
|
878
|
+
const y = h - (v / max) * (h - 4) - 2;
|
|
879
|
+
return `${x.toFixed(1)},${y.toFixed(1)}`;
|
|
880
|
+
}).join(' ');
|
|
881
|
+
const areaPts = `0,${h} ${pts} ${w},${h}`;
|
|
882
|
+
return `<svg viewBox="0 0 ${w} ${h}" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg" style="width:100%;height:100%">
|
|
883
|
+
<defs>
|
|
884
|
+
<linearGradient id="sparkGrad" x1="0" x2="0" y1="0" y2="1">
|
|
885
|
+
<stop offset="0%" stop-color="${color}" stop-opacity="0.45"/>
|
|
886
|
+
<stop offset="100%" stop-color="${color}" stop-opacity="0"/>
|
|
887
|
+
</linearGradient>
|
|
888
|
+
</defs>
|
|
889
|
+
<polygon points="${areaPts}" fill="url(#sparkGrad)"/>
|
|
890
|
+
<polyline points="${pts}" fill="none" stroke="${color}" stroke-width="1.5" stroke-linejoin="round" stroke-linecap="round"/>
|
|
891
|
+
</svg>`;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
// Activity heatmap: 30-day strip, intensity by count
|
|
895
|
+
function heatmapCells(buckets) {
|
|
896
|
+
const max = Math.max(1, ...buckets);
|
|
897
|
+
return buckets.map(c => {
|
|
898
|
+
if (c === 0) return '<div class="ah-cell"></div>';
|
|
899
|
+
const ratio = c / max;
|
|
900
|
+
let level = 1;
|
|
901
|
+
if (ratio > 0.75) level = 4;
|
|
902
|
+
else if (ratio > 0.5) level = 3;
|
|
903
|
+
else if (ratio > 0.25) level = 2;
|
|
904
|
+
return `<div class="ah-cell l${level}" title="${c} task${c===1?'':'s'}"></div>`;
|
|
905
|
+
}).join('');
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
// Agent bubble chart — pack circles in a row, larger = more time
|
|
909
|
+
function agentBubblesSvg(agents) {
|
|
910
|
+
if (!agents.length) return '';
|
|
911
|
+
const W = 600, H = 200;
|
|
912
|
+
const sorted = [...agents].sort((a,b) => b.time_min - a.time_min);
|
|
913
|
+
const maxTime = Math.max(1, sorted[0].time_min);
|
|
914
|
+
// Color palette by agent category
|
|
915
|
+
function colorFor(name) {
|
|
916
|
+
const n = (name || '').toLowerCase();
|
|
917
|
+
if (n.includes('security') || n.includes('compliance')) return '#ff5466';
|
|
918
|
+
if (n.includes('qa') || n.includes('test')) return '#f59e0b';
|
|
919
|
+
if (n.includes('architect') || n.includes('pm')) return '#7c3aed';
|
|
920
|
+
if (n.includes('design')) return '#06b6d4';
|
|
921
|
+
if (n.includes('reviewer')) return '#a78bfa';
|
|
922
|
+
return '#00d97e';
|
|
923
|
+
}
|
|
924
|
+
// Radii proportional to sqrt(time) — area, not linear
|
|
925
|
+
const minR = 22, maxR = 60;
|
|
926
|
+
const radii = sorted.map(a => {
|
|
927
|
+
const ratio = Math.sqrt(a.time_min / maxTime);
|
|
928
|
+
return minR + (maxR - minR) * ratio;
|
|
929
|
+
});
|
|
930
|
+
// Simple horizontal packing
|
|
931
|
+
const padding = 8;
|
|
932
|
+
const totalW = radii.reduce((s, r) => s + 2 * r, 0) + padding * (radii.length - 1);
|
|
933
|
+
const scale = totalW > W ? W / totalW : 1;
|
|
934
|
+
let cx = 0;
|
|
935
|
+
const positions = radii.map(r => {
|
|
936
|
+
const scaledR = r * scale;
|
|
937
|
+
cx += scaledR;
|
|
938
|
+
const pos = { cx, r: scaledR };
|
|
939
|
+
cx += scaledR + padding * scale;
|
|
940
|
+
return pos;
|
|
941
|
+
});
|
|
942
|
+
// Center horizontally
|
|
943
|
+
const usedW = cx - padding * scale;
|
|
944
|
+
const offsetX = (W - usedW) / 2;
|
|
945
|
+
return `<svg class="ab-svg" viewBox="0 0 ${W} ${H}" xmlns="http://www.w3.org/2000/svg">
|
|
946
|
+
${sorted.map((a, i) => {
|
|
947
|
+
const p = positions[i];
|
|
948
|
+
const color = colorFor(a.agent);
|
|
949
|
+
const cx = p.cx + offsetX;
|
|
950
|
+
const cy = H / 2;
|
|
951
|
+
const t = a.time_min >= 60
|
|
952
|
+
? (a.time_min/60).toFixed(a.time_min < 600 ? 1 : 0) + 'h'
|
|
953
|
+
: a.time_min + 'm';
|
|
954
|
+
const fontN = Math.max(9, Math.min(13, p.r / 4));
|
|
955
|
+
const fontM = Math.max(8, Math.min(11, p.r / 5));
|
|
956
|
+
// Truncate name to fit
|
|
957
|
+
const name = a.agent.length > 14 ? a.agent.slice(0, 12) + '…' : a.agent;
|
|
958
|
+
return `<g class="ab-bubble">
|
|
959
|
+
<circle cx="${cx}" cy="${cy}" r="${p.r}" fill="${color}" fill-opacity="0.18" stroke="${color}" stroke-opacity="0.55" stroke-width="1.5"/>
|
|
960
|
+
<text class="ab-bubble-name" x="${cx}" y="${cy - 2}" text-anchor="middle" font-size="${fontN}">${esc(name)}</text>
|
|
961
|
+
<text class="ab-bubble-meta" x="${cx}" y="${cy + fontN + 2}" text-anchor="middle" font-size="${fontM}">${t}</text>
|
|
962
|
+
</g>`;
|
|
963
|
+
}).join('')}
|
|
964
|
+
</svg>`;
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
// Compute datasets
|
|
968
|
+
const dailyDone = bucketByDay(doneTasks, 30);
|
|
969
|
+
const last7Done = dailyDone.slice(-7).reduce((s,v)=>s+v, 0);
|
|
970
|
+
const heatmapHtml = heatmapCells(dailyDone);
|
|
971
|
+
const sparkHtml = sparklineSvg(dailyDone, 280, 56, '#00d97e');
|
|
972
|
+
const bubblesHtml = agentBubblesSvg(agentsCost);
|
|
973
|
+
|
|
974
|
+
// Cost bar chart proportions
|
|
975
|
+
const costMax = Math.max(effectiveHumanUsd, effectiveLlmUsd, 1);
|
|
976
|
+
const humanPct = (effectiveHumanUsd / costMax) * 100;
|
|
977
|
+
const aiPct = Math.max(1.5, (effectiveLlmUsd / costMax) * 100); // min 1.5% so the bar is visible
|
|
978
|
+
|
|
979
|
+
document.getElementById('root').innerHTML = `
|
|
980
|
+
<div class="report-page">
|
|
981
|
+
<div class="report-bg"></div>
|
|
982
|
+
<div class="report-card">
|
|
983
|
+
<div class="report-head">
|
|
984
|
+
<div class="report-brand">
|
|
985
|
+
<span class="brand-mark">
|
|
986
|
+
<svg viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg">
|
|
987
|
+
<g stroke="#00d97e" stroke-width="9" stroke-linecap="round">
|
|
988
|
+
<line x1="32" y1="14" x2="32" y2="50"/>
|
|
989
|
+
<line x1="16.4" y1="23" x2="47.6" y2="41"/>
|
|
990
|
+
<line x1="16.4" y1="41" x2="47.6" y2="23"/>
|
|
991
|
+
</g>
|
|
992
|
+
</svg>
|
|
993
|
+
</span>
|
|
994
|
+
<span>greatcto</span>
|
|
995
|
+
</div>
|
|
996
|
+
<span class="report-status">
|
|
997
|
+
<span class="dot"></span> LIVE
|
|
998
|
+
</span>
|
|
999
|
+
</div>
|
|
1000
|
+
|
|
1001
|
+
<div class="report-title">
|
|
1002
|
+
<div class="stripe"></div>
|
|
1003
|
+
<div>
|
|
1004
|
+
<h1>${esc(project)}</h1>
|
|
1005
|
+
<div class="meta">${esc(date.toUpperCase())} · ENGINEERING REPORT</div>
|
|
1006
|
+
</div>
|
|
1007
|
+
</div>
|
|
1008
|
+
|
|
1009
|
+
${headline ? `<div class="headline-narrative">${esc(headline)}</div>` : ''}
|
|
1010
|
+
|
|
1011
|
+
<!-- HERO BIG — large number + sparkline -->
|
|
1012
|
+
<div class="hero-big">
|
|
1013
|
+
<div class="hero-row">
|
|
1014
|
+
<div class="hero-left">
|
|
1015
|
+
<div class="hero-num">${done || '—'}</div>
|
|
1016
|
+
<div class="hero-lbl">Features shipped</div>
|
|
1017
|
+
<div class="hero-sub">
|
|
1018
|
+
${last7Done > 0
|
|
1019
|
+
? `<span class="delta-up">↑ +${last7Done}</span> in the last 7 days`
|
|
1020
|
+
: `<span style="color:var(--text3)">no shipments in the last 7 days</span>`}
|
|
1021
|
+
</div>
|
|
1022
|
+
</div>
|
|
1023
|
+
<div class="hero-spark">${sparkHtml}</div>
|
|
1024
|
+
</div>
|
|
1025
|
+
</div>
|
|
1026
|
+
|
|
1027
|
+
<!-- COST BAR CHART — visual horizontal bars -->
|
|
1028
|
+
${(effectiveLlmUsd > 0 || effectiveHumanUsd > 0) ? `
|
|
1029
|
+
<div class="cost-bars">
|
|
1030
|
+
<div class="cost-headline">
|
|
1031
|
+
${(() => {
|
|
1032
|
+
// Compute a reliable cost ratio (always positive when both sides are > 0)
|
|
1033
|
+
const costRatio = (effectiveHumanUsd > 0 && effectiveLlmUsd > 0)
|
|
1034
|
+
? effectiveHumanUsd / effectiveLlmUsd : 0;
|
|
1035
|
+
const costRatioStr = costRatio >= 2
|
|
1036
|
+
? Math.round(costRatio).toLocaleString() + '×'
|
|
1037
|
+
: null;
|
|
1038
|
+
if (effectiveSavings) {
|
|
1039
|
+
return `<div class="big">${effectiveSavings} cheaper</div>
|
|
1040
|
+
<div class="lbl">AI vs human team</div>`;
|
|
1041
|
+
}
|
|
1042
|
+
if (costRatioStr) {
|
|
1043
|
+
return `<div class="big">${costRatioStr} cheaper</div>
|
|
1044
|
+
<div class="lbl">AI vs human cost</div>`;
|
|
1045
|
+
}
|
|
1046
|
+
if (timeSpeedup) {
|
|
1047
|
+
return `<div class="big">${timeSpeedup}× faster</div>
|
|
1048
|
+
<div class="lbl">AI vs human time</div>`;
|
|
1049
|
+
}
|
|
1050
|
+
return `<div class="big">AI replaces team</div>
|
|
1051
|
+
<div class="lbl">cost & time breakdown</div>`;
|
|
1052
|
+
})()}
|
|
1053
|
+
</div>
|
|
1054
|
+
<div class="cost-bar-row human">
|
|
1055
|
+
<div class="cb-name">Humans</div>
|
|
1056
|
+
<div class="cb-rail"><div class="cb-fill" style="width:${humanPct.toFixed(1)}%"></div></div>
|
|
1057
|
+
<div class="cb-val">$${Number((effectiveHumanUsd > 0 ? effectiveHumanUsd : humanH * 150).toFixed(0)).toLocaleString()}</div>
|
|
1058
|
+
</div>
|
|
1059
|
+
<div class="cost-bar-row ai">
|
|
1060
|
+
<div class="cb-name">AI</div>
|
|
1061
|
+
<div class="cb-rail"><div class="cb-fill" style="width:${aiPct.toFixed(1)}%"></div></div>
|
|
1062
|
+
<div class="cb-val">$${effectiveLlmUsd.toFixed(2)}</div>
|
|
1063
|
+
</div>
|
|
1064
|
+
</div>` : ''}
|
|
1065
|
+
|
|
1066
|
+
<!-- ACTIVITY HEATMAP — last 30 days -->
|
|
1067
|
+
<div class="activity-heatmap">
|
|
1068
|
+
<div class="ah-head">
|
|
1069
|
+
<span class="ah-title">Daily activity · 30 days</span>
|
|
1070
|
+
<span class="ah-legend">
|
|
1071
|
+
less
|
|
1072
|
+
<span class="ah-legend-cells">
|
|
1073
|
+
<span class="ah-cell"></span>
|
|
1074
|
+
<span class="ah-cell l1"></span>
|
|
1075
|
+
<span class="ah-cell l2"></span>
|
|
1076
|
+
<span class="ah-cell l3"></span>
|
|
1077
|
+
<span class="ah-cell l4"></span>
|
|
1078
|
+
</span>
|
|
1079
|
+
more
|
|
1080
|
+
</span>
|
|
1081
|
+
</div>
|
|
1082
|
+
<div class="ah-grid">${heatmapHtml}</div>
|
|
1083
|
+
</div>
|
|
1084
|
+
|
|
1085
|
+
<div class="report-stats-sec">
|
|
1086
|
+
${sec.map(s => `
|
|
1087
|
+
<div class="report-stat ${s.cls || ''}">
|
|
1088
|
+
<div class="num">${s.v}</div>
|
|
1089
|
+
<div class="lbl">${s.lbl}</div>
|
|
1090
|
+
${s.extra ? `<div class="sub">${s.extra}</div>` : ''}
|
|
1091
|
+
</div>`).join('')}
|
|
1092
|
+
</div>
|
|
1093
|
+
|
|
1094
|
+
${riskItems.length ? `
|
|
1095
|
+
<div class="section-label warn">
|
|
1096
|
+
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>
|
|
1097
|
+
What's at risk · ${riskItems.length} item${riskItems.length === 1 ? '' : 's'}
|
|
1098
|
+
</div>
|
|
1099
|
+
<div class="risk-list">
|
|
1100
|
+
${riskItems.map(t => {
|
|
1101
|
+
const status = blocked.includes(t) ? 'blocked' : (stale.includes(t) ? 'stale' : 'p0');
|
|
1102
|
+
const tag = status === 'blocked' ? 'BLOCKED' : (status === 'stale' ? 'STALE' : 'P0 OPEN');
|
|
1103
|
+
return `
|
|
1104
|
+
<div class="risk-row ${status}">
|
|
1105
|
+
<span class="risk-tag">${tag}</span>
|
|
1106
|
+
<span class="risk-ttl">${esc(t.title)}</span>
|
|
1107
|
+
<span class="risk-meta">${ageStr(t.updated_at || t.created_at)}</span>
|
|
1108
|
+
</div>`;
|
|
1109
|
+
}).join('')}
|
|
1110
|
+
</div>` : ''}
|
|
1111
|
+
|
|
1112
|
+
${coming.length ? `
|
|
1113
|
+
<div class="section-label">Coming up · next ${coming.length} in pipeline</div>
|
|
1114
|
+
<div class="coming-list">
|
|
1115
|
+
${coming.map(t => `
|
|
1116
|
+
<div class="coming-row">
|
|
1117
|
+
<span class="coming-arrow">▸</span>
|
|
1118
|
+
${priChip(t.priority)}
|
|
1119
|
+
<span class="coming-ttl">${esc(t.title)}</span>
|
|
1120
|
+
${t.agent ? `<span class="coming-agent">${esc(t.agent)}</span>` : ''}
|
|
1121
|
+
</div>`).join('')}
|
|
1122
|
+
</div>` : ''}
|
|
1123
|
+
|
|
1124
|
+
${doneTasks.length ? `
|
|
1125
|
+
<div class="section-label">Recently shipped · ${doneTasks.length} item${doneTasks.length === 1 ? '' : 's'}</div>
|
|
1126
|
+
${['security', 'infra', 'feature', 'docs'].filter(c => categories[c].length).map(cat => `
|
|
1127
|
+
<div class="ship-cat">
|
|
1128
|
+
<div class="ship-cat-h">
|
|
1129
|
+
<span class="ship-cat-dot ${cat}"></span>
|
|
1130
|
+
<span class="ship-cat-name">${cat.toUpperCase()}</span>
|
|
1131
|
+
<span class="ship-cat-count">${categories[cat].length}</span>
|
|
1132
|
+
</div>
|
|
1133
|
+
${categories[cat].slice(0, 6).map(t => {
|
|
1134
|
+
const reason = t.close_reason ? esc(t.close_reason.substring(0, 70)) + (t.close_reason.length > 70 ? '…' : '') : '';
|
|
1135
|
+
return `
|
|
1136
|
+
<div class="shipped-row">
|
|
1137
|
+
<span class="shipped-check">✓</span>
|
|
1138
|
+
<span class="ttl">
|
|
1139
|
+
${esc(t.title)}
|
|
1140
|
+
${reason ? `<span class="sub">${reason}</span>` : ''}
|
|
1141
|
+
</span>
|
|
1142
|
+
<span class="when">${whenStr(t.closed_at)}</span>
|
|
1143
|
+
</div>`;
|
|
1144
|
+
}).join('')}
|
|
1145
|
+
</div>`).join('')}` : ''}
|
|
1146
|
+
|
|
1147
|
+
${agentsCost.length ? `
|
|
1148
|
+
<div class="agent-bubbles-wrap">
|
|
1149
|
+
<div class="ab-title">
|
|
1150
|
+
<span>Agent breakdown · time spent</span>
|
|
1151
|
+
<span>${agentsCost.length} agent${agentsCost.length === 1 ? '' : 's'}</span>
|
|
1152
|
+
</div>
|
|
1153
|
+
${bubblesHtml}
|
|
1154
|
+
<div class="ab-foot">
|
|
1155
|
+
<span>Total: ${totalTimeMin >= 60 ? (totalTimeMin/60).toFixed(1)+'h' : totalTimeMin+'m'}</span>
|
|
1156
|
+
<span>LLM $${agentsCost.reduce((s,a)=>s+a.llm_usd,0).toFixed(2)} · Human $${agentsCost.reduce((s,a)=>s+a.human_usd,0).toFixed(0)}</span>
|
|
1157
|
+
<span>${agentsCost.reduce((s,a)=>s+a.tasks_done,0)}/${agentsCost.reduce((s,a)=>s+a.tasks_total,0)} tasks</span>
|
|
1158
|
+
</div>
|
|
1159
|
+
</div>` : ''}
|
|
1160
|
+
|
|
1161
|
+
<div class="trust-foot">
|
|
1162
|
+
<div class="trust-row">
|
|
1163
|
+
<span class="trust-k">Generated</span>
|
|
1164
|
+
<span class="trust-v">${esc(generatedAt)} · live data</span>
|
|
1165
|
+
</div>
|
|
1166
|
+
<div class="trust-row">
|
|
1167
|
+
<span class="trust-k">Project</span>
|
|
1168
|
+
<span class="trust-v">${esc(project)} · greatcto agents</span>
|
|
1169
|
+
</div>
|
|
1170
|
+
<div class="trust-row">
|
|
1171
|
+
<span class="trust-k">Methodology</span>
|
|
1172
|
+
<span class="trust-v">tasks + costs from <code>bd list</code> & <code>docs/plans/PLAN-*.md</code> mtime · <a href="https://github.com/avelikiy/great_cto" target="_blank">view source</a></span>
|
|
1173
|
+
</div>
|
|
1174
|
+
<div class="trust-row">
|
|
1175
|
+
<span class="trust-k">Privacy</span>
|
|
1176
|
+
<span class="trust-v">no source code, credentials, or PII exposed · owner can pause anytime</span>
|
|
1177
|
+
</div>
|
|
1178
|
+
</div>
|
|
1179
|
+
|
|
1180
|
+
<div class="report-foot">
|
|
1181
|
+
CREATED BY <a href="https://greatcto.systems" target="_blank">GREATCTO</a>
|
|
1182
|
+
· AI engineering infrastructure for dev teams
|
|
1183
|
+
· <a href="https://github.com/avelikiy/great_cto" target="_blank">GITHUB</a>
|
|
1184
|
+
</div>
|
|
1185
|
+
</div>
|
|
1186
|
+
</div>`;
|
|
1187
|
+
}
|
|
1188
|
+
</script>
|
|
1189
|
+
</body>
|
|
1190
|
+
</html>
|