agent-trace 0.3.0 → 0.4.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/agent-trace.cjs +2470 -51
- package/package.json +1 -1
package/agent-trace.cjs
CHANGED
|
@@ -24010,6 +24010,7 @@ var import_node_http = __toESM(require("node:http"));
|
|
|
24010
24010
|
// packages/dashboard/src/web-render.ts
|
|
24011
24011
|
function renderDashboardHtml(options = {}) {
|
|
24012
24012
|
const title = options.title ?? "agent-trace dashboard";
|
|
24013
|
+
const currentUserEmailJson = options.currentUserEmail !== void 0 ? JSON.stringify(options.currentUserEmail) : "null";
|
|
24013
24014
|
return `<!doctype html>
|
|
24014
24015
|
<html lang="en">
|
|
24015
24016
|
<head>
|
|
@@ -24141,7 +24142,8 @@ th{color:var(--text-dim);font-size:10px;text-transform:uppercase;letter-spacing:
|
|
|
24141
24142
|
.modal-close:hover{color:var(--text-primary);background:var(--panel-hover)}
|
|
24142
24143
|
.modal label{display:block;font-size:11px;text-transform:uppercase;letter-spacing:.08em;color:var(--text-dim);margin-bottom:4px;margin-top:12px}
|
|
24143
24144
|
.modal select,.modal input{width:100%;box-sizing:border-box;padding:8px 10px;background:var(--bg);border:1px solid var(--panel-border);border-radius:6px;color:var(--text-primary);font-size:12px;font-family:inherit}
|
|
24144
|
-
.modal select:focus,.modal input:focus{outline:none;border-color:var(--green)}
|
|
24145
|
+
.modal select:focus,.modal input:focus,.modal textarea:focus{outline:none;border-color:var(--green)}
|
|
24146
|
+
.modal textarea{width:100%;box-sizing:border-box;padding:8px 10px;background:var(--bg);border:1px solid var(--panel-border);border-radius:6px;color:var(--text-primary);font-size:12px;font-family:inherit;resize:vertical;min-height:80px}
|
|
24145
24147
|
.modal-actions{margin-top:16px;display:flex;gap:8px;align-items:center}
|
|
24146
24148
|
.modal-save{padding:8px 16px;background:var(--green);color:#000;border:none;border-radius:6px;font-size:12px;font-weight:600;cursor:pointer;font-family:inherit}
|
|
24147
24149
|
.modal-save:hover{opacity:.9}
|
|
@@ -24180,8 +24182,92 @@ th{color:var(--text-dim);font-size:10px;text-transform:uppercase;letter-spacing:
|
|
|
24180
24182
|
.hljs-property{color:#93c5fd}
|
|
24181
24183
|
.hljs-addition{color:#4ade80;background:rgba(74,222,128,.08)}
|
|
24182
24184
|
.hljs-deletion{color:#f87171;background:rgba(248,113,113,.08)}
|
|
24183
|
-
|
|
24184
|
-
|
|
24185
|
+
.tab-bar{display:flex;gap:0;margin-top:10px;border-bottom:1px solid var(--line)}
|
|
24186
|
+
.tab-btn{padding:6px 14px;font-size:12px;font-weight:600;color:var(--text-muted);background:none;border:none;border-bottom:2px solid transparent;cursor:pointer;font-family:inherit;letter-spacing:.02em;transition:color .15s,border-color .15s}
|
|
24187
|
+
.tab-btn:hover{color:var(--text-primary)}
|
|
24188
|
+
.tab-btn.active{color:var(--green);border-bottom-color:var(--green)}
|
|
24189
|
+
.tab-content{display:none}.tab-content.active{display:block}
|
|
24190
|
+
.team-mg{margin-top:14px;display:grid;gap:8px;grid-template-columns:repeat(5,minmax(0,1fr))}
|
|
24191
|
+
.budget-bar{margin-top:12px;padding:10px 12px;border:1px solid var(--panel-border);border-radius:8px;background:var(--panel)}
|
|
24192
|
+
.budget-track{height:8px;border-radius:4px;background:var(--panel-muted);margin-top:6px;overflow:hidden}
|
|
24193
|
+
.budget-fill{height:100%;border-radius:4px;transition:width .3s}
|
|
24194
|
+
.budget-fill.green{background:var(--green)}.budget-fill.orange{background:var(--orange)}.budget-fill.red{background:var(--red)}.budget-fill.pulse{animation:pulse 1s infinite}
|
|
24195
|
+
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.5}}
|
|
24196
|
+
.budget-label{font-size:12px;color:var(--text-muted);display:flex;justify-content:space-between;align-items:center}
|
|
24197
|
+
.budget-set-btn{padding:4px 10px;font-size:11px;background:var(--panel-muted);border:1px solid var(--panel-border);border-radius:4px;color:var(--text-muted);cursor:pointer;font-family:inherit}
|
|
24198
|
+
.budget-set-btn:hover{border-color:var(--green);color:var(--green)}
|
|
24199
|
+
.team-table{width:100%;border-collapse:collapse}
|
|
24200
|
+
.team-table th,.team-table td{padding:7px 8px;font-size:12px;border-bottom:1px solid var(--line);text-align:left}
|
|
24201
|
+
.team-table th{color:var(--text-dim);font-size:10px;text-transform:uppercase;letter-spacing:.08em;font-weight:500}
|
|
24202
|
+
.team-row{cursor:pointer}.team-row:hover{background:var(--panel-hover)}
|
|
24203
|
+
.time-range{display:flex;gap:4px;margin-left:auto}
|
|
24204
|
+
.time-range-btn{padding:3px 8px;font-size:10px;background:var(--panel-muted);border:1px solid var(--panel-border);border-radius:4px;color:var(--text-muted);cursor:pointer;font-family:inherit}
|
|
24205
|
+
.time-range-btn.active{border-color:var(--green);color:var(--green)}
|
|
24206
|
+
.filter-chip{display:inline-flex;align-items:center;gap:4px;padding:4px 10px;font-size:12px;font-weight:600;border:1px solid rgba(74,222,128,.4);color:var(--green);border-radius:6px;margin-left:8px;cursor:pointer;background:var(--green-dim)}
|
|
24207
|
+
.filter-chip:hover{background:rgba(74,222,128,.2)}
|
|
24208
|
+
.back-team-link{display:inline-flex;align-items:center;gap:4px;padding:4px 10px;font-size:11px;border:1px solid var(--line);color:var(--text-muted);border-radius:6px;margin-left:8px;cursor:pointer}
|
|
24209
|
+
.back-team-link:hover{background:var(--panel-hover);color:var(--text-primary)}
|
|
24210
|
+
.auth-gate{padding:40px 20px;text-align:center}
|
|
24211
|
+
.auth-gate h2{font-size:16px;color:var(--text-primary);margin-bottom:8px}
|
|
24212
|
+
.auth-gate p{font-size:12px;color:var(--text-muted);margin-bottom:16px}
|
|
24213
|
+
.auth-gate input{width:300px;max-width:80vw;padding:8px 10px;background:var(--bg);border:1px solid var(--panel-border);border-radius:6px;color:var(--text-primary);font-size:12px;font-family:inherit}
|
|
24214
|
+
.auth-gate input:focus{outline:none;border-color:var(--green)}
|
|
24215
|
+
.auth-gate button{margin-top:8px;padding:8px 20px;background:var(--green);color:#000;border:none;border-radius:6px;font-size:12px;font-weight:600;cursor:pointer;font-family:inherit}
|
|
24216
|
+
.auth-gate .auth-error{color:var(--red);font-size:11px;margin-top:8px}
|
|
24217
|
+
/* Insights tab styles */
|
|
24218
|
+
.ins-grid{display:grid;gap:10px;grid-template-columns:1fr 1fr;margin-top:14px}
|
|
24219
|
+
.ins-grid-3{display:grid;gap:10px;grid-template-columns:1fr 1fr 1fr;margin-top:14px}
|
|
24220
|
+
.ins-full{grid-column:1/-1}
|
|
24221
|
+
.ins-summary{margin-top:14px;display:grid;gap:8px;grid-template-columns:repeat(6,minmax(0,1fr))}
|
|
24222
|
+
.ins-panel{border:1px solid var(--panel-border);border-radius:8px;background:var(--panel);overflow:hidden}
|
|
24223
|
+
.ins-ph{padding:10px 12px;border-bottom:1px solid var(--line);display:flex;align-items:center;justify-content:space-between}
|
|
24224
|
+
.ins-ph h3{margin:0;font-size:13px;font-weight:600;letter-spacing:-.01em}
|
|
24225
|
+
.ins-ph p{margin:2px 0 0;font-size:11px;color:var(--text-dim)}
|
|
24226
|
+
.ins-body{padding:12px}
|
|
24227
|
+
.ins-loading{padding:40px 20px;text-align:center;color:var(--text-dim);font-size:12px}
|
|
24228
|
+
.svg-chart{width:100%;overflow:visible}
|
|
24229
|
+
.heatmap-grid{display:grid;grid-template-columns:28px repeat(24,1fr);gap:1px;font-size:9px}
|
|
24230
|
+
.heatmap-cell{aspect-ratio:1;border-radius:2px;background:var(--panel-muted);transition:background .15s}
|
|
24231
|
+
.heatmap-label{display:flex;align-items:center;justify-content:center;color:var(--text-dim);font-size:9px}
|
|
24232
|
+
.hbar-row{display:flex;align-items:center;gap:8px;padding:3px 0}
|
|
24233
|
+
.hbar-label{flex-shrink:0;width:90px;font-size:11px;color:var(--text-muted);text-align:right;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
24234
|
+
.hbar-track{flex:1;height:16px;background:var(--panel-muted);border-radius:3px;overflow:hidden}
|
|
24235
|
+
.hbar-fill{height:100%;border-radius:3px;transition:width .3s}
|
|
24236
|
+
.hbar-value{flex-shrink:0;width:52px;font-size:11px;color:var(--text-dim);font-variant-numeric:tabular-nums}
|
|
24237
|
+
.member-card{border:1px solid var(--panel-border);border-radius:8px;background:var(--panel);overflow:hidden;margin-bottom:10px}
|
|
24238
|
+
.member-card-hd{padding:10px 12px;display:flex;align-items:center;justify-content:space-between;cursor:pointer;user-select:none}
|
|
24239
|
+
.member-card-hd:hover{background:var(--panel-hover)}
|
|
24240
|
+
.member-card-name{font-size:13px;font-weight:600;color:var(--text-primary)}
|
|
24241
|
+
.member-card-email{font-size:10px;color:var(--text-dim)}
|
|
24242
|
+
.member-card-stats{display:flex;gap:6px;font-size:11px}
|
|
24243
|
+
.member-card-body{display:none;border-top:1px solid var(--line);padding:12px}
|
|
24244
|
+
.member-card-body.open{display:block}
|
|
24245
|
+
.member-card-metrics{display:grid;grid-template-columns:repeat(4,1fr);gap:8px;margin-bottom:12px}
|
|
24246
|
+
.member-card-metric{padding:8px;border:1px solid var(--panel-border);border-radius:6px;background:var(--panel-muted)}
|
|
24247
|
+
.member-card-metric .m-label{font-size:9px;text-transform:uppercase;letter-spacing:.08em;color:var(--text-dim)}
|
|
24248
|
+
.member-card-metric .m-val{font-size:16px;font-weight:700;margin-top:2px;font-variant-numeric:tabular-nums}
|
|
24249
|
+
.member-mini-chart{display:flex;align-items:end;gap:1px;height:40px}
|
|
24250
|
+
.member-mini-bar{flex:1;border-radius:1px 1px 0 0;min-height:1px;transition:height .3s}
|
|
24251
|
+
.donut-legend{display:flex;flex-wrap:wrap;gap:8px;margin-top:8px}
|
|
24252
|
+
.donut-legend-item{display:flex;align-items:center;gap:4px;font-size:11px;color:var(--text-muted)}
|
|
24253
|
+
.donut-legend-swatch{width:10px;height:10px;border-radius:2px;flex-shrink:0}
|
|
24254
|
+
.ins-score{width:80px;height:80px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:22px;font-weight:700;border:3px solid;margin:0 auto 8px}
|
|
24255
|
+
.ins-ai-panel{border:1px solid rgba(192,132,252,.2);border-radius:8px;padding:16px;background:rgba(192,132,252,.04);margin-top:14px}
|
|
24256
|
+
.ins-ai-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:8px}
|
|
24257
|
+
.ins-ai-title{font-size:12px;font-weight:600;text-transform:uppercase;letter-spacing:.06em;color:var(--purple)}
|
|
24258
|
+
.configure-analysis-btn{background:var(--panel-muted);border:1px solid var(--panel-border);border-radius:6px;padding:4px 10px;cursor:pointer;color:var(--text-muted);font-size:11px;font-family:inherit;transition:border-color .15s,color .15s}
|
|
24259
|
+
.configure-analysis-btn:hover{border-color:var(--purple);color:var(--purple)}
|
|
24260
|
+
.context-indicator{font-size:10px;color:var(--text-dim);margin-left:8px}
|
|
24261
|
+
.ins-ai-body{font-size:12px;color:var(--text-primary);line-height:1.7}
|
|
24262
|
+
.ins-ai-section{margin-top:12px}
|
|
24263
|
+
.ins-ai-section h4{font-size:11px;text-transform:uppercase;letter-spacing:.06em;color:var(--text-dim);margin:0 0 4px}
|
|
24264
|
+
.ins-ai-item{font-size:12px;color:var(--text-muted);padding:2px 0 2px 14px;position:relative;line-height:1.6}
|
|
24265
|
+
.ins-ai-item::before{content:'>';position:absolute;left:2px;color:var(--purple)}
|
|
24266
|
+
.ins-forecast{border:1px solid rgba(251,146,60,.2);border-radius:8px;padding:12px;background:rgba(251,146,60,.04)}
|
|
24267
|
+
.ins-forecast-val{font-size:24px;font-weight:700;color:var(--orange)}
|
|
24268
|
+
.ins-forecast-label{font-size:11px;color:var(--text-dim);margin-top:2px}
|
|
24269
|
+
@media(max-width:1200px){.mg{grid-template-columns:repeat(2,minmax(0,1fr))}.sg{grid-template-columns:1fr}.team-mg{grid-template-columns:repeat(2,minmax(0,1fr))}.ins-grid{grid-template-columns:1fr}.ins-grid-3{grid-template-columns:1fr}.ins-summary{grid-template-columns:repeat(3,minmax(0,1fr))}}
|
|
24270
|
+
@media(max-width:760px){.shell{padding:12px 8px 24px}.mg{grid-template-columns:1fr}.team-mg{grid-template-columns:1fr}.ins-summary{grid-template-columns:repeat(2,minmax(0,1fr))}.member-card-metrics{grid-template-columns:repeat(2,1fr)}th:nth-child(5),td:nth-child(5),th:nth-child(7),td:nth-child(7){display:none}.erow{grid-template-columns:18px 1fr}.emeta{grid-column:2}.pg-stats{display:none}}
|
|
24185
24271
|
</style>
|
|
24186
24272
|
</head>
|
|
24187
24273
|
<body>
|
|
@@ -24190,8 +24276,21 @@ th{color:var(--text-dim);font-size:10px;text-transform:uppercase;letter-spacing:
|
|
|
24190
24276
|
<h1>${title}</h1>
|
|
24191
24277
|
<p>session observability for coding agents</p>
|
|
24192
24278
|
<button class="settings-btn" onclick="openSettings()"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 15a3 3 0 1 0 0-6 3 3 0 0 0 6 0Z"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1Z"/></svg>AI</button>
|
|
24279
|
+
<div id="tab-bar" class="tab-bar" style="display:none">
|
|
24280
|
+
<button class="tab-btn active" onclick="switchTab('sessions')">Sessions</button>
|
|
24281
|
+
<button class="tab-btn" onclick="switchTab('team')" id="team-tab-btn">Team</button>
|
|
24282
|
+
<button class="tab-btn" onclick="switchTab('insights')" id="insights-tab-btn">Insights</button>
|
|
24283
|
+
</div>
|
|
24193
24284
|
<div id="status" class="status-banner">Connecting...</div>
|
|
24194
24285
|
</section>
|
|
24286
|
+
<div id="auth-gate" class="auth-gate" style="display:none">
|
|
24287
|
+
<h2>Authentication Required</h2>
|
|
24288
|
+
<p>This team server requires an auth token.</p>
|
|
24289
|
+
<input type="password" id="auth-token-input" placeholder="Enter team auth token..." onkeydown="if(event.key==='Enter')submitAuthToken()"/>
|
|
24290
|
+
<br/>
|
|
24291
|
+
<button onclick="submitAuthToken()">Authenticate</button>
|
|
24292
|
+
<div class="auth-error" id="auth-error"></div>
|
|
24293
|
+
</div>
|
|
24195
24294
|
<div id="settings-modal" class="modal-overlay" onclick="if(event.target===this)closeSettings()">
|
|
24196
24295
|
<div class="modal">
|
|
24197
24296
|
<button class="modal-close" onclick="closeSettings()">×</button>
|
|
@@ -24214,6 +24313,7 @@ th{color:var(--text-dim);font-size:10px;text-transform:uppercase;letter-spacing:
|
|
|
24214
24313
|
</div>
|
|
24215
24314
|
</div>
|
|
24216
24315
|
</div>
|
|
24316
|
+
<div id="tab-sessions" class="tab-content active">
|
|
24217
24317
|
<section class="mg">
|
|
24218
24318
|
<article class="mc"><div class="label">Sessions</div><div class="val green" id="m-sessions">0</div></article>
|
|
24219
24319
|
<article class="mc"><div class="label">Total Cost</div><div class="val orange" id="m-cost">$0.00</div></article>
|
|
@@ -24223,7 +24323,7 @@ th{color:var(--text-dim);font-size:10px;text-transform:uppercase;letter-spacing:
|
|
|
24223
24323
|
</section>
|
|
24224
24324
|
<section class="sg">
|
|
24225
24325
|
<section class="panel">
|
|
24226
|
-
<header class="ph"><div><h2>Sessions</h2><p id="stream-label">...</p></div></header>
|
|
24326
|
+
<header class="ph"><div><h2>My Sessions</h2><p id="stream-label">...</p></div></header>
|
|
24227
24327
|
<div class="pc"><div id="sessions-area" class="empty">Loading...</div></div>
|
|
24228
24328
|
</section>
|
|
24229
24329
|
<section class="panel">
|
|
@@ -24235,6 +24335,119 @@ th{color:var(--text-dim);font-size:10px;text-transform:uppercase;letter-spacing:
|
|
|
24235
24335
|
<header class="ph"><div><h2>Session Replay</h2><p id="replay-label">select a session</p></div></header>
|
|
24236
24336
|
<div class="pc" id="replay-area"><div class="empty">No session selected.</div></div>
|
|
24237
24337
|
</section>
|
|
24338
|
+
</div>
|
|
24339
|
+
<div id="tab-team" class="tab-content">
|
|
24340
|
+
<section class="team-mg">
|
|
24341
|
+
<article class="mc"><div class="label">Members</div><div class="val cyan" id="tm-members">0</div></article>
|
|
24342
|
+
<article class="mc"><div class="label">Total Cost</div><div class="val orange" id="tm-cost">$0.00</div></article>
|
|
24343
|
+
<article class="mc"><div class="label">Sessions</div><div class="val green" id="tm-sessions">0</div></article>
|
|
24344
|
+
<article class="mc"><div class="label">Commits</div><div class="val green" id="tm-commits">0</div></article>
|
|
24345
|
+
<article class="mc"><div class="label">$/Commit</div><div class="val" id="tm-cpc">$0.00</div></article>
|
|
24346
|
+
</section>
|
|
24347
|
+
<div id="tm-budget-area" class="budget-bar" style="display:none">
|
|
24348
|
+
<div class="budget-label"><span id="tm-budget-label">Budget</span><button class="budget-set-btn" onclick="openBudgetModal()">Set Budget</button></div>
|
|
24349
|
+
<div class="budget-track"><div class="budget-fill green" id="tm-budget-fill" style="width:0%"></div></div>
|
|
24350
|
+
</div>
|
|
24351
|
+
<div id="tm-no-budget" style="margin-top:12px;text-align:right"><button class="budget-set-btn" onclick="openBudgetModal()">Set Budget</button></div>
|
|
24352
|
+
<section class="sg" style="margin-top:14px">
|
|
24353
|
+
<section class="panel">
|
|
24354
|
+
<header class="ph"><div><h2>Team Members</h2><p id="tm-period-label"></p></div><div class="time-range" id="tm-time-range">
|
|
24355
|
+
<button class="time-range-btn" onclick="setTeamRange('week')">This week</button>
|
|
24356
|
+
<button class="time-range-btn" onclick="setTeamRange('month')">This month</button>
|
|
24357
|
+
<button class="time-range-btn active" onclick="setTeamRange('30d')">Last 30 days</button>
|
|
24358
|
+
</div></header>
|
|
24359
|
+
<div class="pc"><div id="tm-members-area" class="empty">Loading...</div></div>
|
|
24360
|
+
<div id="tm-member-sessions" style="display:none">
|
|
24361
|
+
<header class="ph" style="border-top:1px solid var(--line)"><div><h2 id="tm-member-sessions-title">Sessions</h2><p id="tm-member-sessions-count"></p></div><div><button class="time-range-btn active" onclick="closeMemberSessions()">Back to Team</button></div></header>
|
|
24362
|
+
<div class="pc"><div id="tm-member-sessions-area"></div></div>
|
|
24363
|
+
</div>
|
|
24364
|
+
</section>
|
|
24365
|
+
<section class="panel">
|
|
24366
|
+
<header class="ph"><div><h2>Daily Cost</h2><p>stacked by member</p></div></header>
|
|
24367
|
+
<div class="pc"><div id="tm-cost-chart" class="empty">Loading...</div></div>
|
|
24368
|
+
</section>
|
|
24369
|
+
</section>
|
|
24370
|
+
</div>
|
|
24371
|
+
<div id="tab-insights" class="tab-content">
|
|
24372
|
+
<section class="ins-summary">
|
|
24373
|
+
<article class="mc"><div class="label">Avg $/Session</div><div class="val orange" id="ins-avg-cost">-</div></article>
|
|
24374
|
+
<article class="mc"><div class="label">Avg Commits/Session</div><div class="val green" id="ins-avg-commits">-</div></article>
|
|
24375
|
+
<article class="mc"><div class="label">$/Commit</div><div class="val cyan" id="ins-cost-commit">-</div></article>
|
|
24376
|
+
<article class="mc"><div class="label">Total Tokens</div><div class="val purple" id="ins-tokens">-</div></article>
|
|
24377
|
+
<article class="mc"><div class="label">Efficiency Score</div><div class="val green" id="ins-efficiency">-</div></article>
|
|
24378
|
+
<article class="mc"><div class="label">Forecast (EOM)</div><div class="val orange" id="ins-forecast">-</div></article>
|
|
24379
|
+
</section>
|
|
24380
|
+
<div class="ins-grid" style="margin-top:14px">
|
|
24381
|
+
<div class="ins-panel ins-full">
|
|
24382
|
+
<div class="ins-ph"><div><h3>Cost Trend</h3><p>daily spend with cumulative overlay</p></div><div class="time-range" id="ins-time-range">
|
|
24383
|
+
<button class="time-range-btn" onclick="setInsightsRange('week')">This week</button>
|
|
24384
|
+
<button class="time-range-btn" onclick="setInsightsRange('month')">This month</button>
|
|
24385
|
+
<button class="time-range-btn active" onclick="setInsightsRange('30d')">Last 30 days</button>
|
|
24386
|
+
</div></div>
|
|
24387
|
+
<div class="ins-body" id="ins-cost-trend"><div class="ins-loading">Loading analytics...</div></div>
|
|
24388
|
+
</div>
|
|
24389
|
+
</div>
|
|
24390
|
+
<div class="ins-grid">
|
|
24391
|
+
<div class="ins-panel">
|
|
24392
|
+
<div class="ins-ph"><div><h3>Cost Distribution</h3><p>by team member</p></div></div>
|
|
24393
|
+
<div class="ins-body" id="ins-donut-area"><div class="ins-loading">...</div></div>
|
|
24394
|
+
</div>
|
|
24395
|
+
<div class="ins-panel">
|
|
24396
|
+
<div class="ins-ph"><div><h3>Activity Heatmap</h3><p>sessions by hour & day of week</p></div></div>
|
|
24397
|
+
<div class="ins-body" id="ins-heatmap-area"><div class="ins-loading">...</div></div>
|
|
24398
|
+
</div>
|
|
24399
|
+
</div>
|
|
24400
|
+
<div class="ins-grid">
|
|
24401
|
+
<div class="ins-panel">
|
|
24402
|
+
<div class="ins-ph"><div><h3>Top Models</h3><p>AI models by cost</p></div></div>
|
|
24403
|
+
<div class="ins-body" id="ins-models-area"><div class="ins-loading">...</div></div>
|
|
24404
|
+
</div>
|
|
24405
|
+
<div class="ins-panel">
|
|
24406
|
+
<div class="ins-ph"><div><h3>Top Tools</h3><p>most used tools</p></div></div>
|
|
24407
|
+
<div class="ins-body" id="ins-tools-area"><div class="ins-loading">...</div></div>
|
|
24408
|
+
</div>
|
|
24409
|
+
</div>
|
|
24410
|
+
<div class="ins-panel" style="margin-top:14px">
|
|
24411
|
+
<div class="ins-ph"><div><h3>Per-Member Analytics</h3><p>deep dive into each team member</p></div></div>
|
|
24412
|
+
<div class="ins-body" id="ins-members-area"><div class="ins-loading">...</div></div>
|
|
24413
|
+
</div>
|
|
24414
|
+
<div class="ins-ai-panel" id="ins-ai-area">
|
|
24415
|
+
<div class="ins-ai-header"><div class="ins-ai-title">AI Team Analysis</div><div><span class="context-indicator" id="context-indicator"></span><button class="configure-analysis-btn" onclick="openContextModal()">Configure Analysis</button></div></div>
|
|
24416
|
+
<div id="ins-ai-content">
|
|
24417
|
+
<p style="font-size:12px;color:var(--text-muted);margin:0 0 12px">Generate an AI-powered deep analysis of your team's performance, cost efficiency, and productivity patterns.</p>
|
|
24418
|
+
<button class="insight-gen-btn" id="ins-ai-btn" onclick="generateTeamInsight()">Generate Team Analysis</button>
|
|
24419
|
+
<div id="ins-ai-status" style="font-size:11px;color:var(--text-dim);margin-top:6px"></div>
|
|
24420
|
+
</div>
|
|
24421
|
+
</div>
|
|
24422
|
+
</div>
|
|
24423
|
+
<div id="budget-modal" class="modal-overlay" onclick="if(event.target===this)closeBudgetModal()">
|
|
24424
|
+
<div class="modal">
|
|
24425
|
+
<button class="modal-close" onclick="closeBudgetModal()">×</button>
|
|
24426
|
+
<h3>Set Monthly Budget</h3>
|
|
24427
|
+
<label>Monthly Limit (USD)</label>
|
|
24428
|
+
<input type="number" id="budget-limit" placeholder="e.g. 3500" min="0" step="100"/>
|
|
24429
|
+
<label>Alert Threshold (%)</label>
|
|
24430
|
+
<input type="number" id="budget-threshold" value="80" min="0" max="100" step="5"/>
|
|
24431
|
+
<div class="modal-actions">
|
|
24432
|
+
<button class="modal-save" onclick="saveBudget()">Save Budget</button>
|
|
24433
|
+
<span class="modal-status" id="budget-status"></span>
|
|
24434
|
+
</div>
|
|
24435
|
+
</div>
|
|
24436
|
+
</div>
|
|
24437
|
+
<div id="context-modal" class="modal-overlay" onclick="if(event.target===this)closeContextModal()">
|
|
24438
|
+
<div class="modal" style="width:520px">
|
|
24439
|
+
<button class="modal-close" onclick="closeContextModal()">×</button>
|
|
24440
|
+
<h3>Configure Team Analysis</h3>
|
|
24441
|
+
<label>Company Context</label>
|
|
24442
|
+
<textarea id="context-company" rows="5" placeholder="Describe your company, team structure, project priorities, naming conventions..."></textarea>
|
|
24443
|
+
<label>Analysis Guidelines</label>
|
|
24444
|
+
<textarea id="context-guidelines" rows="5" placeholder="What should the AI focus on? What metrics matter most? Any specific thresholds or criteria for performance reviews?"></textarea>
|
|
24445
|
+
<div class="modal-actions">
|
|
24446
|
+
<button class="modal-save" id="context-save-btn" onclick="saveContext()">Save</button>
|
|
24447
|
+
<span class="modal-status" id="context-status"></span>
|
|
24448
|
+
</div>
|
|
24449
|
+
</div>
|
|
24450
|
+
</div>
|
|
24238
24451
|
</main>
|
|
24239
24452
|
<script>
|
|
24240
24453
|
(function(){
|
|
@@ -24245,6 +24458,316 @@ var costPoints = [];
|
|
|
24245
24458
|
var replay = null;
|
|
24246
24459
|
var insightsConfigured = false;
|
|
24247
24460
|
var insightsCache = {};
|
|
24461
|
+
var teamData = null;
|
|
24462
|
+
var teamRange = '30d';
|
|
24463
|
+
var teamESource = null;
|
|
24464
|
+
var authToken = localStorage.getItem('agent_trace_auth_token') || '';
|
|
24465
|
+
var authRequired = false;
|
|
24466
|
+
var currentTab = 'sessions';
|
|
24467
|
+
var userIdFilter = null;
|
|
24468
|
+
var currentUserEmail = ${currentUserEmailJson};
|
|
24469
|
+
var teamMemberFilter = null;
|
|
24470
|
+
|
|
24471
|
+
function getAuthHeaders() {
|
|
24472
|
+
var h = {};
|
|
24473
|
+
if (authToken) h['Authorization'] = 'Bearer ' + authToken;
|
|
24474
|
+
return h;
|
|
24475
|
+
}
|
|
24476
|
+
|
|
24477
|
+
function authFetch(url, opts) {
|
|
24478
|
+
opts = opts || {};
|
|
24479
|
+
opts.headers = Object.assign({}, opts.headers || {}, getAuthHeaders());
|
|
24480
|
+
return fetch(url, opts);
|
|
24481
|
+
}
|
|
24482
|
+
|
|
24483
|
+
function checkAuth() {
|
|
24484
|
+
return fetch('/api/auth/check', { headers: getAuthHeaders() })
|
|
24485
|
+
.then(function(r) { return r.json(); })
|
|
24486
|
+
.then(function(data) {
|
|
24487
|
+
authRequired = data.authRequired;
|
|
24488
|
+
if (authRequired && !data.authValid) {
|
|
24489
|
+
document.getElementById('auth-gate').style.display = 'block';
|
|
24490
|
+
document.getElementById('tab-sessions').classList.remove('active');
|
|
24491
|
+
return false;
|
|
24492
|
+
}
|
|
24493
|
+
document.getElementById('auth-gate').style.display = 'none';
|
|
24494
|
+
return true;
|
|
24495
|
+
})
|
|
24496
|
+
.catch(function() { return true; });
|
|
24497
|
+
}
|
|
24498
|
+
|
|
24499
|
+
window.submitAuthToken = function() {
|
|
24500
|
+
var input = document.getElementById('auth-token-input');
|
|
24501
|
+
authToken = input.value.trim();
|
|
24502
|
+
localStorage.setItem('agent_trace_auth_token', authToken);
|
|
24503
|
+
document.getElementById('auth-error').textContent = '';
|
|
24504
|
+
checkAuth().then(function(ok) {
|
|
24505
|
+
if (!ok) {
|
|
24506
|
+
document.getElementById('auth-error').textContent = 'Invalid token. Please try again.';
|
|
24507
|
+
} else {
|
|
24508
|
+
document.getElementById('tab-sessions').classList.add('active');
|
|
24509
|
+
startStreaming();
|
|
24510
|
+
}
|
|
24511
|
+
});
|
|
24512
|
+
};
|
|
24513
|
+
|
|
24514
|
+
window.switchTab = function(tab) {
|
|
24515
|
+
currentTab = tab;
|
|
24516
|
+
var btns = document.querySelectorAll('.tab-btn');
|
|
24517
|
+
for (var i = 0; i < btns.length; i++) btns[i].classList.remove('active');
|
|
24518
|
+
if (tab === 'sessions') btns[0].classList.add('active');
|
|
24519
|
+
else if (tab === 'team') btns[1].classList.add('active');
|
|
24520
|
+
else if (tab === 'insights') btns[2].classList.add('active');
|
|
24521
|
+
document.getElementById('tab-sessions').classList.toggle('active', tab === 'sessions');
|
|
24522
|
+
document.getElementById('tab-team').classList.toggle('active', tab === 'team');
|
|
24523
|
+
document.getElementById('tab-insights').classList.toggle('active', tab === 'insights');
|
|
24524
|
+
if (tab === 'team' && !teamESource) startTeamStream();
|
|
24525
|
+
if (tab === 'insights') loadInsightsData();
|
|
24526
|
+
};
|
|
24527
|
+
|
|
24528
|
+
function getTeamDateRange() {
|
|
24529
|
+
var now = new Date();
|
|
24530
|
+
var y = now.getFullYear();
|
|
24531
|
+
var m = now.getMonth();
|
|
24532
|
+
var from, to;
|
|
24533
|
+
if (teamRange === 'week') {
|
|
24534
|
+
var day = now.getDay();
|
|
24535
|
+
var monday = new Date(now);
|
|
24536
|
+
monday.setDate(now.getDate() - (day === 0 ? 6 : day - 1));
|
|
24537
|
+
from = monday.toISOString().slice(0, 10);
|
|
24538
|
+
to = now.toISOString().slice(0, 10);
|
|
24539
|
+
} else if (teamRange === 'month') {
|
|
24540
|
+
from = y + '-' + String(m + 1).padStart(2, '0') + '-01';
|
|
24541
|
+
var lastDay = new Date(y, m + 1, 0).getDate();
|
|
24542
|
+
to = y + '-' + String(m + 1).padStart(2, '0') + '-' + String(lastDay).padStart(2, '0');
|
|
24543
|
+
} else {
|
|
24544
|
+
var d30 = new Date(now);
|
|
24545
|
+
d30.setDate(d30.getDate() - 30);
|
|
24546
|
+
from = d30.toISOString().slice(0, 10);
|
|
24547
|
+
to = now.toISOString().slice(0, 10);
|
|
24548
|
+
}
|
|
24549
|
+
return { from: from, to: to };
|
|
24550
|
+
}
|
|
24551
|
+
|
|
24552
|
+
window.setTeamRange = function(range) {
|
|
24553
|
+
teamRange = range;
|
|
24554
|
+
var btns = document.querySelectorAll('.time-range-btn');
|
|
24555
|
+
for (var i = 0; i < btns.length; i++) btns[i].classList.remove('active');
|
|
24556
|
+
if (range === 'week') btns[0].classList.add('active');
|
|
24557
|
+
else if (range === 'month') btns[1].classList.add('active');
|
|
24558
|
+
else btns[2].classList.add('active');
|
|
24559
|
+
if (teamESource) { teamESource.close(); teamESource = null; }
|
|
24560
|
+
startTeamStream();
|
|
24561
|
+
};
|
|
24562
|
+
|
|
24563
|
+
function startTeamStream() {
|
|
24564
|
+
var r = getTeamDateRange();
|
|
24565
|
+
var qs = '?from=' + r.from + '&to=' + r.to;
|
|
24566
|
+
teamESource = new EventSource('/api/team/stream' + qs);
|
|
24567
|
+
teamESource.addEventListener('team', function(e) {
|
|
24568
|
+
try { teamData = JSON.parse(e.data); renderTeam(); } catch(err) {}
|
|
24569
|
+
});
|
|
24570
|
+
}
|
|
24571
|
+
|
|
24572
|
+
function fmtCost(v) { return '$' + (v||0).toFixed(2); }
|
|
24573
|
+
function fmtNum(v) { return String(v||0).replace(/\\B(?=(\\d{3})+(?!\\d))/g, ','); }
|
|
24574
|
+
|
|
24575
|
+
function timeAgo(isoStr) {
|
|
24576
|
+
if (!isoStr) return '';
|
|
24577
|
+
var diff = Date.now() - new Date(isoStr).getTime();
|
|
24578
|
+
if (diff < 60000) return 'just now';
|
|
24579
|
+
if (diff < 3600000) return Math.floor(diff/60000) + 'm ago';
|
|
24580
|
+
if (diff < 86400000) return Math.floor(diff/3600000) + 'h ago';
|
|
24581
|
+
return Math.floor(diff/86400000) + 'd ago';
|
|
24582
|
+
}
|
|
24583
|
+
|
|
24584
|
+
function renderTeam() {
|
|
24585
|
+
if (!teamData) return;
|
|
24586
|
+
var ov = teamData.overview || {};
|
|
24587
|
+
var mb = teamData.members || {};
|
|
24588
|
+
var cs = teamData.cost || {};
|
|
24589
|
+
var bg = teamData.budget || {};
|
|
24590
|
+
|
|
24591
|
+
document.getElementById('tm-members').textContent = fmtNum(ov.memberCount);
|
|
24592
|
+
document.getElementById('tm-cost').textContent = fmtCost(ov.totalCostUsd);
|
|
24593
|
+
document.getElementById('tm-sessions').textContent = fmtNum(ov.totalSessions);
|
|
24594
|
+
document.getElementById('tm-commits').textContent = fmtNum(ov.totalCommits);
|
|
24595
|
+
document.getElementById('tm-cpc').textContent = fmtCost(ov.costPerCommit);
|
|
24596
|
+
|
|
24597
|
+
// Show team tab if multiple members exist
|
|
24598
|
+
if (ov.memberCount > 1) {
|
|
24599
|
+
document.getElementById('tab-bar').style.display = 'flex';
|
|
24600
|
+
}
|
|
24601
|
+
|
|
24602
|
+
// Budget bar
|
|
24603
|
+
if (bg.budget) {
|
|
24604
|
+
document.getElementById('tm-budget-area').style.display = 'block';
|
|
24605
|
+
document.getElementById('tm-no-budget').style.display = 'none';
|
|
24606
|
+
var pct = bg.percentUsed || 0;
|
|
24607
|
+
var fill = document.getElementById('tm-budget-fill');
|
|
24608
|
+
fill.style.width = Math.min(pct, 100) + '%';
|
|
24609
|
+
fill.className = 'budget-fill';
|
|
24610
|
+
if (pct > 100) { fill.classList.add('red', 'pulse'); }
|
|
24611
|
+
else if (pct > 80) { fill.classList.add('red'); }
|
|
24612
|
+
else if (pct > 60) { fill.classList.add('orange'); }
|
|
24613
|
+
else { fill.classList.add('green'); }
|
|
24614
|
+
document.getElementById('tm-budget-label').textContent = Math.round(pct) + '% of $' + bg.budget.monthlyLimitUsd.toLocaleString() + '/mo (' + fmtCost(bg.currentMonthSpend) + ' spent)';
|
|
24615
|
+
} else {
|
|
24616
|
+
document.getElementById('tm-budget-area').style.display = 'none';
|
|
24617
|
+
document.getElementById('tm-no-budget').style.display = 'block';
|
|
24618
|
+
}
|
|
24619
|
+
|
|
24620
|
+
// Period label
|
|
24621
|
+
if (ov.period) {
|
|
24622
|
+
document.getElementById('tm-period-label').textContent = ov.period.from + ' to ' + ov.period.to;
|
|
24623
|
+
}
|
|
24624
|
+
|
|
24625
|
+
// Members table
|
|
24626
|
+
var members = (mb.members || []);
|
|
24627
|
+
if (members.length === 0) {
|
|
24628
|
+
document.getElementById('tm-members-area').innerHTML = '<div class="empty">No team data yet.</div>';
|
|
24629
|
+
} else {
|
|
24630
|
+
var html = '<table class="team-table"><thead><tr><th>Member</th><th>Sessions</th><th>Cost</th><th>Commits</th><th>Lines</th><th>Last Active</th></tr></thead><tbody>';
|
|
24631
|
+
for (var i = 0; i < members.length; i++) {
|
|
24632
|
+
var m = members[i];
|
|
24633
|
+
var name = m.displayName || m.userId;
|
|
24634
|
+
html += '<tr class="team-row" onclick="filterByMember(\\x27' + (m.userId||'').replace(/'/g,"\\\\'") + '\\x27)">';
|
|
24635
|
+
html += '<td><span style="color:var(--text-primary)">' + esc(name) + '</span>';
|
|
24636
|
+
if (m.displayName && m.userId !== m.displayName) html += '<br/><span style="font-size:10px;color:var(--text-dim)">' + esc(m.userId) + '</span>';
|
|
24637
|
+
html += '</td>';
|
|
24638
|
+
html += '<td>' + m.sessionCount + '</td>';
|
|
24639
|
+
html += '<td class="orange">' + fmtCost(m.totalCostUsd) + '</td>';
|
|
24640
|
+
html += '<td>' + m.commitCount + '</td>';
|
|
24641
|
+
html += '<td><span class="ls green">+' + fmtNum(m.linesAdded) + '</span><span class="ls red">-' + fmtNum(m.linesRemoved) + '</span></td>';
|
|
24642
|
+
html += '<td style="color:var(--text-dim)">' + timeAgo(m.lastActiveAt) + '</td>';
|
|
24643
|
+
html += '</tr>';
|
|
24644
|
+
}
|
|
24645
|
+
html += '</tbody></table>';
|
|
24646
|
+
document.getElementById('tm-members-area').innerHTML = html;
|
|
24647
|
+
}
|
|
24648
|
+
|
|
24649
|
+
// Team daily cost chart (stacked)
|
|
24650
|
+
var points = (cs.points || []).slice(-7);
|
|
24651
|
+
if (points.length === 0) {
|
|
24652
|
+
document.getElementById('tm-cost-chart').innerHTML = '<div class="empty">No cost data.</div>';
|
|
24653
|
+
} else {
|
|
24654
|
+
var maxCost = 0;
|
|
24655
|
+
for (var i = 0; i < points.length; i++) {
|
|
24656
|
+
if (points[i].totalCostUsd > maxCost) maxCost = points[i].totalCostUsd;
|
|
24657
|
+
}
|
|
24658
|
+
var colors = ['var(--green)', 'var(--cyan)', 'var(--orange)', 'var(--purple)', 'var(--yellow)', 'var(--red)', 'var(--text-muted)'];
|
|
24659
|
+
var chartHtml = '<div class="chart">';
|
|
24660
|
+
for (var i = 0; i < points.length; i++) {
|
|
24661
|
+
var p = points[i];
|
|
24662
|
+
var barH = maxCost > 0 ? Math.max(3, Math.round((p.totalCostUsd / maxCost) * 140)) : 3;
|
|
24663
|
+
chartHtml += '<div class="chart-col">';
|
|
24664
|
+
chartHtml += '<div class="chart-value">' + fmtCost(p.totalCostUsd) + '</div>';
|
|
24665
|
+
// Stacked bar segments
|
|
24666
|
+
var bm = p.byMember || [];
|
|
24667
|
+
if (bm.length <= 1) {
|
|
24668
|
+
chartHtml += '<div class="chart-bar" style="height:' + barH + 'px"></div>';
|
|
24669
|
+
} else {
|
|
24670
|
+
chartHtml += '<div style="display:flex;flex-direction:column-reverse;height:' + barH + 'px">';
|
|
24671
|
+
for (var j = 0; j < bm.length; j++) {
|
|
24672
|
+
var segH = maxCost > 0 ? Math.max(1, Math.round((bm[j].totalCostUsd / maxCost) * 140)) : 1;
|
|
24673
|
+
var col = colors[j % colors.length];
|
|
24674
|
+
chartHtml += '<div style="height:' + segH + 'px;background:' + col + ';min-height:1px;border-radius:1px"></div>';
|
|
24675
|
+
}
|
|
24676
|
+
chartHtml += '</div>';
|
|
24677
|
+
}
|
|
24678
|
+
chartHtml += '<div class="chart-label">' + p.date.slice(5) + '</div>';
|
|
24679
|
+
chartHtml += '</div>';
|
|
24680
|
+
}
|
|
24681
|
+
chartHtml += '</div>';
|
|
24682
|
+
document.getElementById('tm-cost-chart').innerHTML = chartHtml;
|
|
24683
|
+
}
|
|
24684
|
+
}
|
|
24685
|
+
|
|
24686
|
+
function esc(s) {
|
|
24687
|
+
if (!s) return '';
|
|
24688
|
+
return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
|
24689
|
+
}
|
|
24690
|
+
|
|
24691
|
+
window.filterByMember = function(userId) {
|
|
24692
|
+
teamMemberFilter = userId;
|
|
24693
|
+
renderMemberSessions();
|
|
24694
|
+
};
|
|
24695
|
+
|
|
24696
|
+
window.clearFilter = function() {
|
|
24697
|
+
userIdFilter = null;
|
|
24698
|
+
renderSessions();
|
|
24699
|
+
};
|
|
24700
|
+
|
|
24701
|
+
window.closeMemberSessions = function() {
|
|
24702
|
+
teamMemberFilter = null;
|
|
24703
|
+
document.getElementById('tm-member-sessions').style.display = 'none';
|
|
24704
|
+
};
|
|
24705
|
+
|
|
24706
|
+
function renderMemberSessions() {
|
|
24707
|
+
if (!teamMemberFilter) return;
|
|
24708
|
+
var memberSessions = sessions.filter(function(s) { return s.userId === teamMemberFilter; });
|
|
24709
|
+
var memberName = teamMemberFilter;
|
|
24710
|
+
memberSessions.forEach(function(s) { if (s.userDisplayName) memberName = s.userDisplayName; });
|
|
24711
|
+
document.getElementById('tm-member-sessions-title').textContent = memberName + ' \u2014 Sessions';
|
|
24712
|
+
document.getElementById('tm-member-sessions-count').textContent = memberSessions.length + ' sessions';
|
|
24713
|
+
document.getElementById('tm-member-sessions').style.display = 'block';
|
|
24714
|
+
var area = document.getElementById('tm-member-sessions-area');
|
|
24715
|
+
if (memberSessions.length === 0) {
|
|
24716
|
+
area.innerHTML = '<div class="empty">No sessions for this member.</div>';
|
|
24717
|
+
return;
|
|
24718
|
+
}
|
|
24719
|
+
var h = '<table><thead><tr><th>Session</th><th>Repo</th><th>Started</th><th>Prompts</th><th>Cost</th><th>Commits</th><th>Lines</th></tr></thead><tbody>';
|
|
24720
|
+
memberSessions.forEach(function(s) {
|
|
24721
|
+
var repo = s.gitRepo ? (s.gitBranch ? s.gitRepo + '/' + s.gitBranch : s.gitRepo) : '-';
|
|
24722
|
+
var commits = s.commitCount > 0 ? '<span class="badge green">' + s.commitCount + '</span>' : '<span class="badge dim">0</span>';
|
|
24723
|
+
var lines = (s.linesAdded > 0 || s.linesRemoved > 0) ? '<span class="ls green">+' + s.linesAdded + '</span><span class="ls red">-' + s.linesRemoved + '</span>' : '<span style="color:var(--text-dim)">-</span>';
|
|
24724
|
+
h += '<tr class="srow" data-sid="' + esc(s.sessionId) + '" onclick="selectSession(this.dataset.sid);switchTab(\\x27sessions\\x27)"><td>' + esc(s.sessionId.slice(0,10)) + '</td><td class="repo-cell">' + esc(repo) + '</td><td>' + fmtDate(s.startedAt) + '</td><td>' + s.promptCount + '</td><td>' + fmt$(s.totalCostUsd) + '</td><td>' + commits + '</td><td>' + lines + '</td></tr>';
|
|
24725
|
+
});
|
|
24726
|
+
h += '</tbody></table>';
|
|
24727
|
+
area.innerHTML = h;
|
|
24728
|
+
document.getElementById('tm-member-sessions').scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
24729
|
+
}
|
|
24730
|
+
|
|
24731
|
+
window.openBudgetModal = function() {
|
|
24732
|
+
document.getElementById('budget-modal').classList.add('open');
|
|
24733
|
+
if (teamData && teamData.budget && teamData.budget.budget) {
|
|
24734
|
+
document.getElementById('budget-limit').value = teamData.budget.budget.monthlyLimitUsd;
|
|
24735
|
+
document.getElementById('budget-threshold').value = teamData.budget.budget.alertThresholdPercent;
|
|
24736
|
+
}
|
|
24737
|
+
};
|
|
24738
|
+
|
|
24739
|
+
window.closeBudgetModal = function() {
|
|
24740
|
+
document.getElementById('budget-modal').classList.remove('open');
|
|
24741
|
+
};
|
|
24742
|
+
|
|
24743
|
+
window.saveBudget = function() {
|
|
24744
|
+
var limit = parseFloat(document.getElementById('budget-limit').value);
|
|
24745
|
+
var threshold = parseFloat(document.getElementById('budget-threshold').value) || 80;
|
|
24746
|
+
if (isNaN(limit) || limit < 0) {
|
|
24747
|
+
document.getElementById('budget-status').textContent = 'Invalid limit';
|
|
24748
|
+
document.getElementById('budget-status').className = 'modal-status error';
|
|
24749
|
+
return;
|
|
24750
|
+
}
|
|
24751
|
+
document.getElementById('budget-status').textContent = 'Saving...';
|
|
24752
|
+
document.getElementById('budget-status').className = 'modal-status';
|
|
24753
|
+
authFetch('/api/team/budget', {
|
|
24754
|
+
method: 'POST',
|
|
24755
|
+
headers: { 'Content-Type': 'application/json' },
|
|
24756
|
+
body: JSON.stringify({ monthlyLimitUsd: limit, alertThresholdPercent: threshold })
|
|
24757
|
+
}).then(function(r) { return r.json(); }).then(function(data) {
|
|
24758
|
+
if (data.status === 'ok') {
|
|
24759
|
+
document.getElementById('budget-status').textContent = 'Saved!';
|
|
24760
|
+
document.getElementById('budget-status').className = 'modal-status ok';
|
|
24761
|
+
setTimeout(closeBudgetModal, 800);
|
|
24762
|
+
} else {
|
|
24763
|
+
document.getElementById('budget-status').textContent = data.message || 'Error';
|
|
24764
|
+
document.getElementById('budget-status').className = 'modal-status error';
|
|
24765
|
+
}
|
|
24766
|
+
}).catch(function() {
|
|
24767
|
+
document.getElementById('budget-status').textContent = 'Network error';
|
|
24768
|
+
document.getElementById('budget-status').className = 'modal-status error';
|
|
24769
|
+
});
|
|
24770
|
+
};
|
|
24248
24771
|
|
|
24249
24772
|
window.openSettings = function() {
|
|
24250
24773
|
document.getElementById('settings-modal').classList.add('open');
|
|
@@ -24545,9 +25068,16 @@ window.togglePrompt = function(hd) {
|
|
|
24545
25068
|
|
|
24546
25069
|
function renderSessions() {
|
|
24547
25070
|
var area = document.getElementById('sessions-area');
|
|
24548
|
-
|
|
25071
|
+
var label = document.getElementById('stream-label');
|
|
25072
|
+
var filtered = sessions;
|
|
25073
|
+
// Sessions tab always shows only the current user's sessions
|
|
25074
|
+
if (currentUserEmail) {
|
|
25075
|
+
filtered = sessions.filter(function(s) { return s.userId === currentUserEmail; });
|
|
25076
|
+
label.textContent = filtered.length + ' sessions (you)';
|
|
25077
|
+
}
|
|
25078
|
+
if (filtered.length === 0) { area.innerHTML = '<div class="empty">' + (currentUserEmail ? 'No sessions for you yet. Start a Claude Code session.' : 'No sessions captured yet.') + '</div>'; return; }
|
|
24549
25079
|
var h = '<table><thead><tr><th>Session</th><th>Repo</th><th>Started</th><th>Prompts</th><th>Cost</th><th>Commits</th><th>Lines</th></tr></thead><tbody>';
|
|
24550
|
-
|
|
25080
|
+
filtered.forEach(function(s){
|
|
24551
25081
|
var active = s.sessionId === selectedId ? ' active' : '';
|
|
24552
25082
|
var repo = s.gitRepo ? (s.gitBranch ? s.gitRepo + '/' + s.gitBranch : s.gitRepo) : '-';
|
|
24553
25083
|
var commits = s.commitCount > 0 ? '<span class="badge green">' + s.commitCount + '</span>' : '<span class="badge dim">0</span>';
|
|
@@ -24559,14 +25089,15 @@ function renderSessions() {
|
|
|
24559
25089
|
}
|
|
24560
25090
|
|
|
24561
25091
|
function renderMetrics() {
|
|
24562
|
-
|
|
24563
|
-
document.getElementById('m-
|
|
24564
|
-
document.getElementById('m-
|
|
24565
|
-
document.getElementById('m-
|
|
24566
|
-
|
|
24567
|
-
var
|
|
25092
|
+
var my = currentUserEmail ? sessions.filter(function(s){return s.userId === currentUserEmail;}) : sessions;
|
|
25093
|
+
document.getElementById('m-sessions').textContent = my.length;
|
|
25094
|
+
document.getElementById('m-cost').textContent = fmt$(my.reduce(function(s,x){return s+x.totalCostUsd;},0));
|
|
25095
|
+
document.getElementById('m-prompts').textContent = my.reduce(function(s,x){return s+x.promptCount;},0);
|
|
25096
|
+
document.getElementById('m-tools').textContent = my.reduce(function(s,x){return s+x.toolCallCount;},0);
|
|
25097
|
+
var tc = my.reduce(function(s,x){return s+x.commitCount;},0);
|
|
25098
|
+
var sc = my.filter(function(x){return x.commitCount>0;}).length;
|
|
24568
25099
|
document.getElementById('m-commits').textContent = tc;
|
|
24569
|
-
document.getElementById('m-commits-det').textContent = sc + '/' +
|
|
25100
|
+
document.getElementById('m-commits-det').textContent = sc + '/' + my.length + ' sessions produced commits';
|
|
24570
25101
|
}
|
|
24571
25102
|
|
|
24572
25103
|
function renderCostChart() {
|
|
@@ -24670,8 +25201,10 @@ function setSessions(raw) {
|
|
|
24670
25201
|
sessions = sortLatest(raw.map(parseSummary).filter(Boolean));
|
|
24671
25202
|
renderMetrics();
|
|
24672
25203
|
renderSessions();
|
|
24673
|
-
if (
|
|
24674
|
-
|
|
25204
|
+
if (teamMemberFilter) renderMemberSessions();
|
|
25205
|
+
var my = currentUserEmail ? sessions.filter(function(s){return s.userId === currentUserEmail;}) : sessions;
|
|
25206
|
+
if (my.length > 0 && (!selectedId || !my.some(function(s){return s.sessionId===selectedId;}))) {
|
|
25207
|
+
selectSession(my[0].sessionId);
|
|
24675
25208
|
}
|
|
24676
25209
|
}
|
|
24677
25210
|
|
|
@@ -24716,7 +25249,7 @@ function loadSnapshot() {
|
|
|
24716
25249
|
});
|
|
24717
25250
|
}
|
|
24718
25251
|
|
|
24719
|
-
function
|
|
25252
|
+
function startStreaming() {
|
|
24720
25253
|
fetch('/api/settings/insights',{cache:'no-store'}).then(function(r){return r.json();}).then(function(data){
|
|
24721
25254
|
if(data && data.configured) insightsConfigured = true;
|
|
24722
25255
|
}).catch(function(){});
|
|
@@ -24730,6 +25263,12 @@ function boot() {
|
|
|
24730
25263
|
document.getElementById('stream-label').textContent = 'live';
|
|
24731
25264
|
document.getElementById('status').className = 'status-banner';
|
|
24732
25265
|
document.getElementById('status').textContent = 'Live';
|
|
25266
|
+
// Auto-detect multi-user and show team tab
|
|
25267
|
+
var userIds = {};
|
|
25268
|
+
sessions.forEach(function(s){ if(s.userId && s.userId !== 'unknown_user') userIds[s.userId] = 1; });
|
|
25269
|
+
if (Object.keys(userIds).length > 1) {
|
|
25270
|
+
document.getElementById('tab-bar').style.display = 'flex';
|
|
25271
|
+
}
|
|
24733
25272
|
}
|
|
24734
25273
|
});
|
|
24735
25274
|
es.addEventListener('bridge_error', function(event) {
|
|
@@ -24747,6 +25286,645 @@ function boot() {
|
|
|
24747
25286
|
});
|
|
24748
25287
|
}
|
|
24749
25288
|
|
|
25289
|
+
// ===== INSIGHTS TAB =====
|
|
25290
|
+
var insightsData = null;
|
|
25291
|
+
var insightsRange = '30d';
|
|
25292
|
+
var insightsLoading = false;
|
|
25293
|
+
var MEMBER_COLORS = ['#4ade80','#22d3ee','#fb923c','#c084fc','#facc15','#f87171','#60a5fa','#a78bfa','#34d399','#fbbf24'];
|
|
25294
|
+
|
|
25295
|
+
function getInsightsDateRange() {
|
|
25296
|
+
var now = new Date();
|
|
25297
|
+
var y = now.getFullYear();
|
|
25298
|
+
var m = now.getMonth();
|
|
25299
|
+
var from, to;
|
|
25300
|
+
if (insightsRange === 'week') {
|
|
25301
|
+
var day = now.getDay();
|
|
25302
|
+
var monday = new Date(now);
|
|
25303
|
+
monday.setDate(now.getDate() - (day === 0 ? 6 : day - 1));
|
|
25304
|
+
from = monday.toISOString().slice(0, 10);
|
|
25305
|
+
to = now.toISOString().slice(0, 10);
|
|
25306
|
+
} else if (insightsRange === 'month') {
|
|
25307
|
+
from = y + '-' + String(m + 1).padStart(2, '0') + '-01';
|
|
25308
|
+
var lastDay = new Date(y, m + 1, 0).getDate();
|
|
25309
|
+
to = y + '-' + String(m + 1).padStart(2, '0') + '-' + String(lastDay).padStart(2, '0');
|
|
25310
|
+
} else {
|
|
25311
|
+
var d30 = new Date(now);
|
|
25312
|
+
d30.setDate(d30.getDate() - 30);
|
|
25313
|
+
from = d30.toISOString().slice(0, 10);
|
|
25314
|
+
to = now.toISOString().slice(0, 10);
|
|
25315
|
+
}
|
|
25316
|
+
return { from: from, to: to };
|
|
25317
|
+
}
|
|
25318
|
+
|
|
25319
|
+
window.setInsightsRange = function(range) {
|
|
25320
|
+
insightsRange = range;
|
|
25321
|
+
var btns = document.querySelectorAll('#ins-time-range .time-range-btn');
|
|
25322
|
+
for (var i = 0; i < btns.length; i++) btns[i].classList.remove('active');
|
|
25323
|
+
if (range === 'week') btns[0].classList.add('active');
|
|
25324
|
+
else if (range === 'month') btns[1].classList.add('active');
|
|
25325
|
+
else btns[2].classList.add('active');
|
|
25326
|
+
insightsData = null;
|
|
25327
|
+
loadInsightsData();
|
|
25328
|
+
};
|
|
25329
|
+
|
|
25330
|
+
function loadInsightsData() {
|
|
25331
|
+
if (insightsLoading) return;
|
|
25332
|
+
if (insightsData) { renderInsights(); return; }
|
|
25333
|
+
insightsLoading = true;
|
|
25334
|
+
var r = getInsightsDateRange();
|
|
25335
|
+
authFetch('/api/team/analytics?from=' + r.from + '&to=' + r.to)
|
|
25336
|
+
.then(function(res) { return res.json(); })
|
|
25337
|
+
.then(function(data) {
|
|
25338
|
+
insightsLoading = false;
|
|
25339
|
+
if (data && data.status === 'ok') {
|
|
25340
|
+
insightsData = data;
|
|
25341
|
+
renderInsights();
|
|
25342
|
+
}
|
|
25343
|
+
})
|
|
25344
|
+
.catch(function() { insightsLoading = false; });
|
|
25345
|
+
}
|
|
25346
|
+
|
|
25347
|
+
function renderInsights() {
|
|
25348
|
+
if (!insightsData) return;
|
|
25349
|
+
var d = insightsData;
|
|
25350
|
+
|
|
25351
|
+
// Summary cards
|
|
25352
|
+
document.getElementById('ins-avg-cost').textContent = '$' + (d.avgCostPerSession || 0).toFixed(2);
|
|
25353
|
+
document.getElementById('ins-avg-commits').textContent = (d.avgCommitsPerSession || 0).toFixed(1);
|
|
25354
|
+
document.getElementById('ins-cost-commit').textContent = '$' + (d.avgCostPerCommit || 0).toFixed(2);
|
|
25355
|
+
document.getElementById('ins-tokens').textContent = formatLargeNum(d.totalTokensUsed || 0);
|
|
25356
|
+
document.getElementById('ins-efficiency').textContent = (d.costEfficiencyScore || 0) + '/100';
|
|
25357
|
+
|
|
25358
|
+
// Forecast: linear extrapolation to end of month
|
|
25359
|
+
var forecast = computeForecast(d.costTrend || []);
|
|
25360
|
+
document.getElementById('ins-forecast').textContent = '$' + forecast.toFixed(0);
|
|
25361
|
+
|
|
25362
|
+
renderCostTrendChart(d.costTrend || []);
|
|
25363
|
+
renderDonutChart(d.memberAnalytics || []);
|
|
25364
|
+
renderHeatmap(d.hourlyHeatmap || []);
|
|
25365
|
+
renderTopModels(d.topModels || []);
|
|
25366
|
+
renderTopTools(d.topTools || []);
|
|
25367
|
+
renderMemberCards(d.memberAnalytics || []);
|
|
25368
|
+
}
|
|
25369
|
+
|
|
25370
|
+
function formatLargeNum(n) {
|
|
25371
|
+
if (n >= 1000000) return (n / 1000000).toFixed(1) + 'M';
|
|
25372
|
+
if (n >= 1000) return (n / 1000).toFixed(1) + 'K';
|
|
25373
|
+
return String(n);
|
|
25374
|
+
}
|
|
25375
|
+
|
|
25376
|
+
function computeForecast(costTrend) {
|
|
25377
|
+
if (!costTrend || costTrend.length < 2) return 0;
|
|
25378
|
+
var now = new Date();
|
|
25379
|
+
var daysInMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate();
|
|
25380
|
+
var dayOfMonth = now.getDate();
|
|
25381
|
+
var totalSoFar = 0;
|
|
25382
|
+
for (var i = 0; i < costTrend.length; i++) totalSoFar += costTrend[i].costUsd;
|
|
25383
|
+
if (dayOfMonth === 0) return totalSoFar;
|
|
25384
|
+
return (totalSoFar / dayOfMonth) * daysInMonth;
|
|
25385
|
+
}
|
|
25386
|
+
|
|
25387
|
+
function renderCostTrendChart(trend) {
|
|
25388
|
+
var el = document.getElementById('ins-cost-trend');
|
|
25389
|
+
if (!trend || trend.length === 0) { el.innerHTML = '<div class="ins-loading">No cost data available.</div>'; return; }
|
|
25390
|
+
|
|
25391
|
+
var W = 800, H = 200, PL = 50, PR = 50, PT = 20, PB = 30;
|
|
25392
|
+
var cw = W - PL - PR, ch = H - PT - PB;
|
|
25393
|
+
var maxDaily = 0.01, maxCum = 0.01;
|
|
25394
|
+
for (var i = 0; i < trend.length; i++) {
|
|
25395
|
+
if (trend[i].costUsd > maxDaily) maxDaily = trend[i].costUsd;
|
|
25396
|
+
if (trend[i].cumulativeCostUsd > maxCum) maxCum = trend[i].cumulativeCostUsd;
|
|
25397
|
+
}
|
|
25398
|
+
|
|
25399
|
+
var svg = '<svg class="svg-chart" viewBox="0 0 ' + W + ' ' + H + '" preserveAspectRatio="none">';
|
|
25400
|
+
|
|
25401
|
+
// Grid lines
|
|
25402
|
+
for (var g = 0; g <= 4; g++) {
|
|
25403
|
+
var gy = PT + (ch / 4) * g;
|
|
25404
|
+
svg += '<line x1="' + PL + '" y1="' + gy + '" x2="' + (W - PR) + '" y2="' + gy + '" stroke="#1a1a1a" stroke-width="1"/>';
|
|
25405
|
+
var label = '$' + ((maxDaily * (4 - g) / 4)).toFixed(2);
|
|
25406
|
+
svg += '<text x="' + (PL - 4) + '" y="' + (gy + 3) + '" text-anchor="end" fill="#444" font-size="9" font-family="inherit">' + label + '</text>';
|
|
25407
|
+
}
|
|
25408
|
+
|
|
25409
|
+
// Bars (daily cost)
|
|
25410
|
+
var barW = Math.max(4, (cw / trend.length) - 2);
|
|
25411
|
+
for (var i = 0; i < trend.length; i++) {
|
|
25412
|
+
var x = PL + (i / trend.length) * cw + 1;
|
|
25413
|
+
var barH = (trend[i].costUsd / maxDaily) * ch;
|
|
25414
|
+
var y = PT + ch - barH;
|
|
25415
|
+
svg += '<rect x="' + x + '" y="' + y + '" width="' + barW + '" height="' + barH + '" rx="2" fill="rgba(74,222,128,0.3)"/>';
|
|
25416
|
+
// Date label every N bars
|
|
25417
|
+
if (trend.length <= 14 || i % Math.ceil(trend.length / 10) === 0) {
|
|
25418
|
+
svg += '<text x="' + (x + barW / 2) + '" y="' + (H - 4) + '" text-anchor="middle" fill="#444" font-size="8" font-family="inherit">' + trend[i].date.slice(5) + '</text>';
|
|
25419
|
+
}
|
|
25420
|
+
}
|
|
25421
|
+
|
|
25422
|
+
// Cumulative line
|
|
25423
|
+
var linePts = '';
|
|
25424
|
+
var areaPts = '';
|
|
25425
|
+
for (var i = 0; i < trend.length; i++) {
|
|
25426
|
+
var lx = PL + (i / (trend.length - 1 || 1)) * cw;
|
|
25427
|
+
var ly = PT + ch - (trend[i].cumulativeCostUsd / maxCum) * ch;
|
|
25428
|
+
linePts += (i === 0 ? 'M' : 'L') + lx + ',' + ly;
|
|
25429
|
+
areaPts += (i === 0 ? 'M' : 'L') + lx + ',' + ly;
|
|
25430
|
+
}
|
|
25431
|
+
// Area fill
|
|
25432
|
+
areaPts += 'L' + (PL + cw) + ',' + (PT + ch) + 'L' + PL + ',' + (PT + ch) + 'Z';
|
|
25433
|
+
svg += '<defs><linearGradient id="cumGrad" x1="0" y1="0" x2="0" y2="1"><stop offset="0%" stop-color="rgba(34,211,238,0.15)"/><stop offset="100%" stop-color="rgba(34,211,238,0)"/></linearGradient></defs>';
|
|
25434
|
+
svg += '<path d="' + areaPts + '" fill="url(#cumGrad)"/>';
|
|
25435
|
+
svg += '<path d="' + linePts + '" fill="none" stroke="#22d3ee" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>';
|
|
25436
|
+
|
|
25437
|
+
// Dots on cumulative line
|
|
25438
|
+
for (var i = 0; i < trend.length; i++) {
|
|
25439
|
+
var dx = PL + (i / (trend.length - 1 || 1)) * cw;
|
|
25440
|
+
var dy = PT + ch - (trend[i].cumulativeCostUsd / maxCum) * ch;
|
|
25441
|
+
svg += '<circle cx="' + dx + '" cy="' + dy + '" r="3" fill="#22d3ee" stroke="#000" stroke-width="1"/>';
|
|
25442
|
+
}
|
|
25443
|
+
|
|
25444
|
+
// Right Y axis label for cumulative
|
|
25445
|
+
svg += '<text x="' + (W - 4) + '" y="' + (PT + 3) + '" text-anchor="end" fill="#22d3ee" font-size="9" font-family="inherit">$' + maxCum.toFixed(0) + ' total</text>';
|
|
25446
|
+
|
|
25447
|
+
svg += '</svg>';
|
|
25448
|
+
svg += '<div style="display:flex;gap:16px;margin-top:4px;font-size:10px;color:var(--text-dim)">';
|
|
25449
|
+
svg += '<span><span style="display:inline-block;width:10px;height:10px;background:rgba(74,222,128,0.3);border-radius:2px;vertical-align:middle;margin-right:3px"></span>Daily cost</span>';
|
|
25450
|
+
svg += '<span><span style="display:inline-block;width:10px;height:2px;background:#22d3ee;vertical-align:middle;margin-right:3px"></span>Cumulative</span>';
|
|
25451
|
+
svg += '</div>';
|
|
25452
|
+
el.innerHTML = svg;
|
|
25453
|
+
}
|
|
25454
|
+
|
|
25455
|
+
function renderDonutChart(members) {
|
|
25456
|
+
var el = document.getElementById('ins-donut-area');
|
|
25457
|
+
if (!members || members.length === 0) { el.innerHTML = '<div class="ins-loading">No member data.</div>'; return; }
|
|
25458
|
+
|
|
25459
|
+
var total = 0;
|
|
25460
|
+
for (var i = 0; i < members.length; i++) total += members[i].totalCostUsd;
|
|
25461
|
+
if (total <= 0) { el.innerHTML = '<div class="ins-loading">No cost data.</div>'; return; }
|
|
25462
|
+
|
|
25463
|
+
var R = 70, r = 42, cx = 90, cy = 90;
|
|
25464
|
+
var svg = '<svg width="180" height="180" viewBox="0 0 180 180">';
|
|
25465
|
+
var startAngle = -Math.PI / 2;
|
|
25466
|
+
|
|
25467
|
+
for (var i = 0; i < members.length; i++) {
|
|
25468
|
+
var pct = members[i].totalCostUsd / total;
|
|
25469
|
+
var endAngle = startAngle + pct * 2 * Math.PI;
|
|
25470
|
+
var largeArc = pct > 0.5 ? 1 : 0;
|
|
25471
|
+
var x1 = cx + R * Math.cos(startAngle);
|
|
25472
|
+
var y1 = cy + R * Math.sin(startAngle);
|
|
25473
|
+
var x2 = cx + R * Math.cos(endAngle);
|
|
25474
|
+
var y2 = cy + R * Math.sin(endAngle);
|
|
25475
|
+
var ix1 = cx + r * Math.cos(endAngle);
|
|
25476
|
+
var iy1 = cy + r * Math.sin(endAngle);
|
|
25477
|
+
var ix2 = cx + r * Math.cos(startAngle);
|
|
25478
|
+
var iy2 = cy + r * Math.sin(startAngle);
|
|
25479
|
+
var col = MEMBER_COLORS[i % MEMBER_COLORS.length];
|
|
25480
|
+
svg += '<path d="M' + x1 + ',' + y1 + ' A' + R + ',' + R + ' 0 ' + largeArc + ',1 ' + x2 + ',' + y2 + ' L' + ix1 + ',' + iy1 + ' A' + r + ',' + r + ' 0 ' + largeArc + ',0 ' + ix2 + ',' + iy2 + 'Z" fill="' + col + '" opacity="0.85"/>';
|
|
25481
|
+
startAngle = endAngle;
|
|
25482
|
+
}
|
|
25483
|
+
|
|
25484
|
+
// Center label
|
|
25485
|
+
svg += '<text x="' + cx + '" y="' + (cy - 4) + '" text-anchor="middle" fill="#d4d4d4" font-size="16" font-weight="700" font-family="inherit">$' + total.toFixed(0) + '</text>';
|
|
25486
|
+
svg += '<text x="' + cx + '" y="' + (cy + 10) + '" text-anchor="middle" fill="#666" font-size="9" font-family="inherit">total</text>';
|
|
25487
|
+
svg += '</svg>';
|
|
25488
|
+
|
|
25489
|
+
// Legend
|
|
25490
|
+
var legend = '<div class="donut-legend">';
|
|
25491
|
+
for (var i = 0; i < members.length; i++) {
|
|
25492
|
+
var m = members[i];
|
|
25493
|
+
var pctLabel = ((m.totalCostUsd / total) * 100).toFixed(0);
|
|
25494
|
+
legend += '<div class="donut-legend-item"><div class="donut-legend-swatch" style="background:' + MEMBER_COLORS[i % MEMBER_COLORS.length] + '"></div>' + esc(m.displayName || m.userId) + ' (' + pctLabel + '%, $' + m.totalCostUsd.toFixed(2) + ')</div>';
|
|
25495
|
+
}
|
|
25496
|
+
legend += '</div>';
|
|
25497
|
+
el.innerHTML = '<div style="display:flex;align-items:flex-start;gap:20px;flex-wrap:wrap"><div>' + svg + '</div><div style="flex:1;min-width:160px">' + legend + '</div></div>';
|
|
25498
|
+
}
|
|
25499
|
+
|
|
25500
|
+
function renderHeatmap(heatmap) {
|
|
25501
|
+
var el = document.getElementById('ins-heatmap-area');
|
|
25502
|
+
if (!heatmap || heatmap.length === 0) { el.innerHTML = '<div class="ins-loading">No activity data.</div>'; return; }
|
|
25503
|
+
|
|
25504
|
+
var maxCount = 1;
|
|
25505
|
+
for (var i = 0; i < heatmap.length; i++) {
|
|
25506
|
+
if (heatmap[i].count > maxCount) maxCount = heatmap[i].count;
|
|
25507
|
+
}
|
|
25508
|
+
|
|
25509
|
+
var dayLabels = ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'];
|
|
25510
|
+
var grid = {};
|
|
25511
|
+
for (var i = 0; i < heatmap.length; i++) {
|
|
25512
|
+
grid[heatmap[i].day + '-' + heatmap[i].hour] = heatmap[i].count;
|
|
25513
|
+
}
|
|
25514
|
+
|
|
25515
|
+
var h = '<div class="heatmap-grid">';
|
|
25516
|
+
// Header row
|
|
25517
|
+
h += '<div class="heatmap-label"></div>';
|
|
25518
|
+
for (var hr = 0; hr < 24; hr++) {
|
|
25519
|
+
h += '<div class="heatmap-label">' + (hr % 3 === 0 ? hr + 'h' : '') + '</div>';
|
|
25520
|
+
}
|
|
25521
|
+
// Data rows
|
|
25522
|
+
for (var d = 1; d <= 7; d++) {
|
|
25523
|
+
var dayIdx = d % 7; // Mon=1 -> dayIdx=1, Sun=0 -> dayIdx=0
|
|
25524
|
+
h += '<div class="heatmap-label">' + dayLabels[dayIdx] + '</div>';
|
|
25525
|
+
for (var hr = 0; hr < 24; hr++) {
|
|
25526
|
+
var count = grid[dayIdx + '-' + hr] || 0;
|
|
25527
|
+
var intensity = count / maxCount;
|
|
25528
|
+
var bg;
|
|
25529
|
+
if (count === 0) bg = 'var(--panel-muted)';
|
|
25530
|
+
else if (intensity < 0.25) bg = 'rgba(74,222,128,0.15)';
|
|
25531
|
+
else if (intensity < 0.5) bg = 'rgba(74,222,128,0.3)';
|
|
25532
|
+
else if (intensity < 0.75) bg = 'rgba(74,222,128,0.55)';
|
|
25533
|
+
else bg = 'rgba(74,222,128,0.8)';
|
|
25534
|
+
h += '<div class="heatmap-cell" style="background:' + bg + '" title="' + dayLabels[dayIdx] + ' ' + hr + ':00 - ' + count + ' sessions"></div>';
|
|
25535
|
+
}
|
|
25536
|
+
}
|
|
25537
|
+
h += '</div>';
|
|
25538
|
+
h += '<div style="display:flex;align-items:center;gap:4px;margin-top:6px;font-size:9px;color:var(--text-dim)"><span>Less</span>';
|
|
25539
|
+
h += '<div style="width:10px;height:10px;background:var(--panel-muted);border-radius:2px"></div>';
|
|
25540
|
+
h += '<div style="width:10px;height:10px;background:rgba(74,222,128,0.15);border-radius:2px"></div>';
|
|
25541
|
+
h += '<div style="width:10px;height:10px;background:rgba(74,222,128,0.3);border-radius:2px"></div>';
|
|
25542
|
+
h += '<div style="width:10px;height:10px;background:rgba(74,222,128,0.55);border-radius:2px"></div>';
|
|
25543
|
+
h += '<div style="width:10px;height:10px;background:rgba(74,222,128,0.8);border-radius:2px"></div>';
|
|
25544
|
+
h += '<span>More</span></div>';
|
|
25545
|
+
el.innerHTML = h;
|
|
25546
|
+
}
|
|
25547
|
+
|
|
25548
|
+
function renderTopModels(models) {
|
|
25549
|
+
var el = document.getElementById('ins-models-area');
|
|
25550
|
+
if (!models || models.length === 0) { el.innerHTML = '<div class="ins-loading">No model data.</div>'; return; }
|
|
25551
|
+
|
|
25552
|
+
var maxCost = 0.01;
|
|
25553
|
+
for (var i = 0; i < models.length; i++) {
|
|
25554
|
+
if (models[i].totalCostUsd > maxCost) maxCost = models[i].totalCostUsd;
|
|
25555
|
+
}
|
|
25556
|
+
|
|
25557
|
+
var h = '';
|
|
25558
|
+
for (var i = 0; i < models.length; i++) {
|
|
25559
|
+
var m = models[i];
|
|
25560
|
+
var pct = (m.totalCostUsd / maxCost) * 100;
|
|
25561
|
+
var col = MEMBER_COLORS[i % MEMBER_COLORS.length];
|
|
25562
|
+
h += '<div class="hbar-row">';
|
|
25563
|
+
h += '<div class="hbar-label" title="' + esc(m.model) + '">' + esc((m.model.indexOf('/') >= 0 ? m.model.split('/').pop() : m.model)) + '</div>';
|
|
25564
|
+
h += '<div class="hbar-track"><div class="hbar-fill" style="width:' + pct + '%;background:' + col + '"></div></div>';
|
|
25565
|
+
h += '<div class="hbar-value">$' + m.totalCostUsd.toFixed(2) + '</div>';
|
|
25566
|
+
h += '</div>';
|
|
25567
|
+
}
|
|
25568
|
+
el.innerHTML = h;
|
|
25569
|
+
}
|
|
25570
|
+
|
|
25571
|
+
function renderTopTools(tools) {
|
|
25572
|
+
var el = document.getElementById('ins-tools-area');
|
|
25573
|
+
if (!tools || tools.length === 0) { el.innerHTML = '<div class="ins-loading">No tool data.</div>'; return; }
|
|
25574
|
+
|
|
25575
|
+
var maxCalls = 1;
|
|
25576
|
+
for (var i = 0; i < tools.length; i++) {
|
|
25577
|
+
if (tools[i].callCount > maxCalls) maxCalls = tools[i].callCount;
|
|
25578
|
+
}
|
|
25579
|
+
|
|
25580
|
+
var h = '';
|
|
25581
|
+
var shown = tools.slice(0, 10);
|
|
25582
|
+
for (var i = 0; i < shown.length; i++) {
|
|
25583
|
+
var t = shown[i];
|
|
25584
|
+
var pct = (t.callCount / maxCalls) * 100;
|
|
25585
|
+
var col = MEMBER_COLORS[i % MEMBER_COLORS.length];
|
|
25586
|
+
h += '<div class="hbar-row">';
|
|
25587
|
+
h += '<div class="hbar-label">' + esc(t.tool) + '</div>';
|
|
25588
|
+
h += '<div class="hbar-track"><div class="hbar-fill" style="width:' + pct + '%;background:' + col + '"></div></div>';
|
|
25589
|
+
h += '<div class="hbar-value">' + fmtNum(t.callCount) + '</div>';
|
|
25590
|
+
h += '</div>';
|
|
25591
|
+
}
|
|
25592
|
+
el.innerHTML = h;
|
|
25593
|
+
}
|
|
25594
|
+
|
|
25595
|
+
function renderMemberCards(members) {
|
|
25596
|
+
var el = document.getElementById('ins-members-area');
|
|
25597
|
+
if (!members || members.length === 0) { el.innerHTML = '<div class="ins-loading">No member data.</div>'; return; }
|
|
25598
|
+
|
|
25599
|
+
var h = '';
|
|
25600
|
+
for (var i = 0; i < members.length; i++) {
|
|
25601
|
+
var m = members[i];
|
|
25602
|
+
var col = MEMBER_COLORS[i % MEMBER_COLORS.length];
|
|
25603
|
+
var name = m.displayName || m.userId;
|
|
25604
|
+
h += '<div class="member-card">';
|
|
25605
|
+
h += '<div class="member-card-hd" onclick="toggleMemberCard(this)">';
|
|
25606
|
+
h += '<div><div class="member-card-name" style="color:' + col + '">' + esc(name) + '</div>';
|
|
25607
|
+
if (m.displayName && m.userId !== m.displayName) h += '<div class="member-card-email">' + esc(m.userId) + '</div>';
|
|
25608
|
+
h += '</div>';
|
|
25609
|
+
h += '<div class="member-card-stats">';
|
|
25610
|
+
h += '<span class="badge orange">' + fmtCost(m.totalCostUsd) + '</span>';
|
|
25611
|
+
h += '<span class="badge green">' + m.sessionCount + ' sessions</span>';
|
|
25612
|
+
h += '<span class="badge">' + m.commitCount + ' commits</span>';
|
|
25613
|
+
h += '</div>';
|
|
25614
|
+
h += '</div>';
|
|
25615
|
+
h += '<div class="member-card-body" id="mc-body-' + i + '">';
|
|
25616
|
+
|
|
25617
|
+
// Metrics grid
|
|
25618
|
+
h += '<div class="member-card-metrics">';
|
|
25619
|
+
h += '<div class="member-card-metric"><div class="m-label">Avg $/Session</div><div class="m-val orange">$' + (m.avgSessionCostUsd || 0).toFixed(2) + '</div></div>';
|
|
25620
|
+
h += '<div class="member-card-metric"><div class="m-label">$/Commit</div><div class="m-val cyan">$' + (m.costPerCommit || 0).toFixed(2) + '</div></div>';
|
|
25621
|
+
h += '<div class="member-card-metric"><div class="m-label">Lines Changed</div><div class="m-val"><span class="green">+' + fmtNum(m.linesAdded) + '</span> <span class="red">-' + fmtNum(m.linesRemoved) + '</span></div></div>';
|
|
25622
|
+
h += '<div class="member-card-metric"><div class="m-label">Pull Requests</div><div class="m-val purple">' + m.prCount + '</div></div>';
|
|
25623
|
+
h += '</div>';
|
|
25624
|
+
|
|
25625
|
+
// Tokens
|
|
25626
|
+
h += '<div style="margin-bottom:12px;font-size:11px;color:var(--text-dim)">';
|
|
25627
|
+
h += '<span style="margin-right:12px">Input tokens: <span class="cyan">' + formatLargeNum(m.totalInputTokens) + '</span></span>';
|
|
25628
|
+
h += '<span>Output tokens: <span class="cyan">' + formatLargeNum(m.totalOutputTokens) + '</span></span>';
|
|
25629
|
+
h += '</div>';
|
|
25630
|
+
|
|
25631
|
+
// Models used
|
|
25632
|
+
if (m.models && m.models.length > 0) {
|
|
25633
|
+
h += '<div style="margin-bottom:10px"><div style="font-size:10px;text-transform:uppercase;letter-spacing:.08em;color:var(--text-dim);margin-bottom:4px">Models</div>';
|
|
25634
|
+
for (var j = 0; j < m.models.length; j++) {
|
|
25635
|
+
var mdl = m.models[j];
|
|
25636
|
+
h += '<div class="hbar-row"><div class="hbar-label" style="width:120px">' + esc((mdl.model.indexOf('/') >= 0 ? mdl.model.split('/').pop() : mdl.model)) + '</div>';
|
|
25637
|
+
var modelMax = m.models[0].totalCostUsd || 0.01;
|
|
25638
|
+
h += '<div class="hbar-track"><div class="hbar-fill" style="width:' + ((mdl.totalCostUsd / modelMax) * 100) + '%;background:' + col + '"></div></div>';
|
|
25639
|
+
h += '<div class="hbar-value">$' + mdl.totalCostUsd.toFixed(2) + '</div></div>';
|
|
25640
|
+
}
|
|
25641
|
+
h += '</div>';
|
|
25642
|
+
}
|
|
25643
|
+
|
|
25644
|
+
// Repos
|
|
25645
|
+
if (m.repos && m.repos.length > 0) {
|
|
25646
|
+
h += '<div style="margin-bottom:10px"><div style="font-size:10px;text-transform:uppercase;letter-spacing:.08em;color:var(--text-dim);margin-bottom:4px">Repositories</div>';
|
|
25647
|
+
for (var j = 0; j < m.repos.length; j++) {
|
|
25648
|
+
var rp = m.repos[j];
|
|
25649
|
+
h += '<div style="display:flex;align-items:center;gap:8px;padding:2px 0;font-size:11px">';
|
|
25650
|
+
h += '<span style="color:var(--text-primary)">' + esc(rp.repo) + '</span>';
|
|
25651
|
+
h += '<span class="badge">' + rp.sessionCount + ' sessions</span>';
|
|
25652
|
+
h += '<span class="badge green">' + rp.commitCount + ' commits</span>';
|
|
25653
|
+
h += '</div>';
|
|
25654
|
+
}
|
|
25655
|
+
h += '</div>';
|
|
25656
|
+
}
|
|
25657
|
+
|
|
25658
|
+
// Activity mini-charts (hourly)
|
|
25659
|
+
h += '<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px">';
|
|
25660
|
+
// Hourly
|
|
25661
|
+
h += '<div><div style="font-size:10px;text-transform:uppercase;letter-spacing:.08em;color:var(--text-dim);margin-bottom:4px">Hourly Activity</div>';
|
|
25662
|
+
h += '<div class="member-mini-chart">';
|
|
25663
|
+
var maxHour = 1;
|
|
25664
|
+
if (m.hourlyActivity) { for (var hr = 0; hr < 24; hr++) { if (m.hourlyActivity[hr] > maxHour) maxHour = m.hourlyActivity[hr]; } }
|
|
25665
|
+
for (var hr = 0; hr < 24; hr++) {
|
|
25666
|
+
var val = (m.hourlyActivity && m.hourlyActivity[hr]) || 0;
|
|
25667
|
+
var barH = val > 0 ? Math.max(2, Math.round((val / maxHour) * 36)) : 1;
|
|
25668
|
+
h += '<div class="member-mini-bar" style="height:' + barH + 'px;background:' + (val > 0 ? col : 'var(--panel-muted)') + '"></div>';
|
|
25669
|
+
}
|
|
25670
|
+
h += '</div><div style="display:flex;justify-content:space-between;font-size:8px;color:var(--text-dim);margin-top:2px"><span>0h</span><span>12h</span><span>23h</span></div>';
|
|
25671
|
+
h += '</div>';
|
|
25672
|
+
|
|
25673
|
+
// Daily
|
|
25674
|
+
h += '<div><div style="font-size:10px;text-transform:uppercase;letter-spacing:.08em;color:var(--text-dim);margin-bottom:4px">Daily Activity</div>';
|
|
25675
|
+
h += '<div class="member-mini-chart" style="gap:3px">';
|
|
25676
|
+
var dayNames = ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'];
|
|
25677
|
+
var maxDay = 1;
|
|
25678
|
+
if (m.dailyActivity) { for (var dd = 0; dd < 7; dd++) { if (m.dailyActivity[dd] > maxDay) maxDay = m.dailyActivity[dd]; } }
|
|
25679
|
+
for (var dd = 0; dd < 7; dd++) {
|
|
25680
|
+
var val = (m.dailyActivity && m.dailyActivity[dd]) || 0;
|
|
25681
|
+
var barH = val > 0 ? Math.max(2, Math.round((val / maxDay) * 36)) : 1;
|
|
25682
|
+
h += '<div style="display:flex;flex-direction:column;align-items:center;flex:1"><div class="member-mini-bar" style="height:' + barH + 'px;background:' + (val > 0 ? col : 'var(--panel-muted)') + ';width:100%"></div><div style="font-size:8px;color:var(--text-dim);margin-top:2px">' + dayNames[dd].charAt(0) + '</div></div>';
|
|
25683
|
+
}
|
|
25684
|
+
h += '</div>';
|
|
25685
|
+
h += '</div>';
|
|
25686
|
+
h += '</div>';
|
|
25687
|
+
|
|
25688
|
+
h += '</div>'; // member-card-body
|
|
25689
|
+
h += '</div>'; // member-card
|
|
25690
|
+
}
|
|
25691
|
+
el.innerHTML = h;
|
|
25692
|
+
}
|
|
25693
|
+
|
|
25694
|
+
window.toggleMemberCard = function(hd) {
|
|
25695
|
+
var body = hd.nextElementSibling;
|
|
25696
|
+
body.classList.toggle('open');
|
|
25697
|
+
};
|
|
25698
|
+
|
|
25699
|
+
// ===== AI TEAM INSIGHTS =====
|
|
25700
|
+
var teamInsightData = null;
|
|
25701
|
+
|
|
25702
|
+
window.generateTeamInsight = function(force) {
|
|
25703
|
+
var btn = document.getElementById('ins-ai-btn');
|
|
25704
|
+
var status = document.getElementById('ins-ai-status');
|
|
25705
|
+
var content = document.getElementById('ins-ai-content');
|
|
25706
|
+
if (!btn) return;
|
|
25707
|
+
|
|
25708
|
+
btn.disabled = true;
|
|
25709
|
+
btn.textContent = 'Analyzing...';
|
|
25710
|
+
if (status) {
|
|
25711
|
+
status.textContent = 'Sending team data to AI provider. This may take 15-30 seconds...';
|
|
25712
|
+
status.style.color = 'var(--text-dim)';
|
|
25713
|
+
}
|
|
25714
|
+
|
|
25715
|
+
var r = getInsightsDateRange();
|
|
25716
|
+
var forceQs = force ? '&force=true' : '';
|
|
25717
|
+
authFetch('/api/team/insights/generate?from=' + r.from + '&to=' + r.to + forceQs, {
|
|
25718
|
+
method: 'POST',
|
|
25719
|
+
headers: { 'Content-Type': 'application/json' },
|
|
25720
|
+
body: '{}'
|
|
25721
|
+
})
|
|
25722
|
+
.then(function(res) { return res.json().then(function(d) { return { ok: res.ok, data: d }; }); })
|
|
25723
|
+
.then(function(res) {
|
|
25724
|
+
btn.disabled = false;
|
|
25725
|
+
btn.textContent = 'Regenerate Team Analysis';
|
|
25726
|
+
if (status) status.textContent = '';
|
|
25727
|
+
if (res.ok && res.data.status === 'ok' && res.data.insight) {
|
|
25728
|
+
teamInsightData = res.data.insight;
|
|
25729
|
+
renderTeamInsight(res.data.insight);
|
|
25730
|
+
} else {
|
|
25731
|
+
var msg = (res.data && res.data.message) ? res.data.message : 'Failed to generate team insight';
|
|
25732
|
+
if (status) {
|
|
25733
|
+
status.textContent = msg;
|
|
25734
|
+
status.style.color = 'var(--red)';
|
|
25735
|
+
}
|
|
25736
|
+
}
|
|
25737
|
+
})
|
|
25738
|
+
.catch(function(e) {
|
|
25739
|
+
btn.disabled = false;
|
|
25740
|
+
btn.textContent = 'Generate Team Analysis';
|
|
25741
|
+
if (status) {
|
|
25742
|
+
status.textContent = String(e);
|
|
25743
|
+
status.style.color = 'var(--red)';
|
|
25744
|
+
}
|
|
25745
|
+
});
|
|
25746
|
+
};
|
|
25747
|
+
|
|
25748
|
+
function renderTeamInsight(insight) {
|
|
25749
|
+
var el = document.getElementById('ins-ai-content');
|
|
25750
|
+
if (!el) return;
|
|
25751
|
+
|
|
25752
|
+
var h = '';
|
|
25753
|
+
|
|
25754
|
+
// Header with provider info
|
|
25755
|
+
h += '<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:12px">';
|
|
25756
|
+
h += '<div style="font-size:10px;color:var(--text-dim)">' + esc(insight.provider || '') + ' / ' + esc(insight.model || '') + ' — ' + new Date(insight.generatedAt).toLocaleString() + '</div>';
|
|
25757
|
+
h += '<button class="insight-gen-btn" onclick="generateTeamInsight(true)" style="font-size:10px;padding:4px 10px">Regenerate</button>';
|
|
25758
|
+
h += '</div>';
|
|
25759
|
+
|
|
25760
|
+
// Executive Summary
|
|
25761
|
+
h += '<div style="border:1px solid rgba(192,132,252,.15);border-radius:8px;padding:12px;margin-bottom:14px;background:rgba(192,132,252,.06)">';
|
|
25762
|
+
h += '<div style="font-size:10px;text-transform:uppercase;letter-spacing:.08em;color:var(--purple);margin-bottom:6px;font-weight:600">Executive Summary</div>';
|
|
25763
|
+
h += '<div style="font-size:13px;color:var(--text-primary);line-height:1.7">' + esc(insight.executiveSummary) + '</div>';
|
|
25764
|
+
h += '</div>';
|
|
25765
|
+
|
|
25766
|
+
// Cost & Productivity Analysis side by side
|
|
25767
|
+
h += '<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:14px">';
|
|
25768
|
+
|
|
25769
|
+
// Cost Analysis
|
|
25770
|
+
h += '<div style="border:1px solid rgba(251,146,60,.15);border-radius:8px;padding:12px;background:rgba(251,146,60,.04)">';
|
|
25771
|
+
h += '<div style="font-size:10px;text-transform:uppercase;letter-spacing:.08em;color:var(--orange);margin-bottom:6px;font-weight:600">Cost Analysis</div>';
|
|
25772
|
+
h += '<div style="font-size:12px;color:var(--text-primary);line-height:1.7">' + esc(insight.costAnalysis) + '</div>';
|
|
25773
|
+
h += '</div>';
|
|
25774
|
+
|
|
25775
|
+
// Productivity Analysis
|
|
25776
|
+
h += '<div style="border:1px solid rgba(74,222,128,.15);border-radius:8px;padding:12px;background:rgba(74,222,128,.04)">';
|
|
25777
|
+
h += '<div style="font-size:10px;text-transform:uppercase;letter-spacing:.08em;color:var(--green);margin-bottom:6px;font-weight:600">Productivity Analysis</div>';
|
|
25778
|
+
h += '<div style="font-size:12px;color:var(--text-primary);line-height:1.7">' + esc(insight.productivityAnalysis) + '</div>';
|
|
25779
|
+
h += '</div>';
|
|
25780
|
+
|
|
25781
|
+
h += '</div>';
|
|
25782
|
+
|
|
25783
|
+
// Member Highlights
|
|
25784
|
+
if (insight.memberHighlights && insight.memberHighlights.length > 0) {
|
|
25785
|
+
h += '<div style="margin-bottom:14px">';
|
|
25786
|
+
h += '<div style="font-size:10px;text-transform:uppercase;letter-spacing:.08em;color:var(--text-dim);margin-bottom:8px;font-weight:600">Member Highlights</div>';
|
|
25787
|
+
for (var i = 0; i < insight.memberHighlights.length; i++) {
|
|
25788
|
+
var mh = insight.memberHighlights[i];
|
|
25789
|
+
var col = MEMBER_COLORS[i % MEMBER_COLORS.length];
|
|
25790
|
+
h += '<div style="border:1px solid var(--panel-border);border-radius:8px;padding:10px 12px;margin-bottom:8px;background:var(--panel);border-left:3px solid ' + col + '">';
|
|
25791
|
+
h += '<div style="font-size:12px;font-weight:600;color:' + col + ';margin-bottom:4px">' + esc(mh.displayName || mh.userId) + '</div>';
|
|
25792
|
+
h += '<div style="font-size:11px;color:var(--text-muted);line-height:1.6">';
|
|
25793
|
+
h += '<div style="margin-bottom:3px"><span style="color:var(--green);font-weight:600">Strength:</span> ' + esc(mh.strength) + '</div>';
|
|
25794
|
+
if (mh.concern) {
|
|
25795
|
+
h += '<div style="margin-bottom:3px"><span style="color:var(--orange);font-weight:600">Watch:</span> ' + esc(mh.concern) + '</div>';
|
|
25796
|
+
}
|
|
25797
|
+
h += '<div><span style="color:var(--cyan);font-weight:600">Recommendation:</span> ' + esc(mh.recommendation) + '</div>';
|
|
25798
|
+
h += '</div>';
|
|
25799
|
+
h += '</div>';
|
|
25800
|
+
}
|
|
25801
|
+
h += '</div>';
|
|
25802
|
+
}
|
|
25803
|
+
|
|
25804
|
+
// Risks and Recommendations side by side
|
|
25805
|
+
h += '<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:14px">';
|
|
25806
|
+
|
|
25807
|
+
// Risks
|
|
25808
|
+
if (insight.risks && insight.risks.length > 0) {
|
|
25809
|
+
h += '<div style="border:1px solid rgba(248,113,113,.15);border-radius:8px;padding:12px;background:rgba(248,113,113,.04)">';
|
|
25810
|
+
h += '<div style="font-size:10px;text-transform:uppercase;letter-spacing:.08em;color:var(--red);margin-bottom:8px;font-weight:600">Risks</div>';
|
|
25811
|
+
for (var i = 0; i < insight.risks.length; i++) {
|
|
25812
|
+
h += '<div class="ins-ai-item">' + esc(insight.risks[i]) + '</div>';
|
|
25813
|
+
}
|
|
25814
|
+
h += '</div>';
|
|
25815
|
+
} else {
|
|
25816
|
+
h += '<div></div>';
|
|
25817
|
+
}
|
|
25818
|
+
|
|
25819
|
+
// Recommendations
|
|
25820
|
+
if (insight.recommendations && insight.recommendations.length > 0) {
|
|
25821
|
+
h += '<div style="border:1px solid rgba(34,211,238,.15);border-radius:8px;padding:12px;background:rgba(34,211,238,.04)">';
|
|
25822
|
+
h += '<div style="font-size:10px;text-transform:uppercase;letter-spacing:.08em;color:var(--cyan);margin-bottom:8px;font-weight:600">Recommendations</div>';
|
|
25823
|
+
for (var i = 0; i < insight.recommendations.length; i++) {
|
|
25824
|
+
h += '<div class="ins-ai-item" style="color:var(--text-muted)">' + esc(insight.recommendations[i]) + '</div>';
|
|
25825
|
+
}
|
|
25826
|
+
h += '</div>';
|
|
25827
|
+
} else {
|
|
25828
|
+
h += '<div></div>';
|
|
25829
|
+
}
|
|
25830
|
+
|
|
25831
|
+
h += '</div>';
|
|
25832
|
+
|
|
25833
|
+
// Forecast
|
|
25834
|
+
if (insight.forecast) {
|
|
25835
|
+
h += '<div class="ins-forecast" style="margin-bottom:8px">';
|
|
25836
|
+
h += '<div style="font-size:10px;text-transform:uppercase;letter-spacing:.08em;color:var(--orange);margin-bottom:4px;font-weight:600">Forecast</div>';
|
|
25837
|
+
h += '<div style="font-size:12px;color:var(--text-primary);line-height:1.6">' + esc(insight.forecast) + '</div>';
|
|
25838
|
+
h += '</div>';
|
|
25839
|
+
}
|
|
25840
|
+
|
|
25841
|
+
el.innerHTML = h;
|
|
25842
|
+
}
|
|
25843
|
+
|
|
25844
|
+
// ===== TEAM INSIGHTS CONTEXT =====
|
|
25845
|
+
window.openContextModal = function() {
|
|
25846
|
+
var modal = document.getElementById('context-modal');
|
|
25847
|
+
if (!modal) return;
|
|
25848
|
+
var statusEl = document.getElementById('context-status');
|
|
25849
|
+
if (statusEl) statusEl.textContent = '';
|
|
25850
|
+
authFetch('/api/team/insights/context')
|
|
25851
|
+
.then(function(r) { return r.json(); })
|
|
25852
|
+
.then(function(data) {
|
|
25853
|
+
if (data.configured && data.context) {
|
|
25854
|
+
var co = document.getElementById('context-company');
|
|
25855
|
+
var gu = document.getElementById('context-guidelines');
|
|
25856
|
+
if (co) co.value = data.context.companyContext || '';
|
|
25857
|
+
if (gu) gu.value = data.context.analysisGuidelines || '';
|
|
25858
|
+
}
|
|
25859
|
+
modal.classList.add('open');
|
|
25860
|
+
})
|
|
25861
|
+
.catch(function() { modal.classList.add('open'); });
|
|
25862
|
+
};
|
|
25863
|
+
|
|
25864
|
+
window.closeContextModal = function() {
|
|
25865
|
+
var modal = document.getElementById('context-modal');
|
|
25866
|
+
if (modal) modal.classList.remove('open');
|
|
25867
|
+
};
|
|
25868
|
+
|
|
25869
|
+
window.saveContext = function() {
|
|
25870
|
+
var btn = document.getElementById('context-save-btn');
|
|
25871
|
+
var statusEl = document.getElementById('context-status');
|
|
25872
|
+
var co = document.getElementById('context-company');
|
|
25873
|
+
var gu = document.getElementById('context-guidelines');
|
|
25874
|
+
if (!co || !gu) return;
|
|
25875
|
+
var body = { companyContext: co.value, analysisGuidelines: gu.value };
|
|
25876
|
+
if (!body.companyContext && !body.analysisGuidelines) {
|
|
25877
|
+
if (statusEl) { statusEl.textContent = 'Enter at least one field'; statusEl.className = 'modal-status error'; }
|
|
25878
|
+
return;
|
|
25879
|
+
}
|
|
25880
|
+
if (btn) btn.disabled = true;
|
|
25881
|
+
if (statusEl) { statusEl.textContent = 'Saving...'; statusEl.className = 'modal-status'; }
|
|
25882
|
+
authFetch('/api/team/insights/context', {
|
|
25883
|
+
method: 'POST',
|
|
25884
|
+
headers: { 'Content-Type': 'application/json' },
|
|
25885
|
+
body: JSON.stringify(body)
|
|
25886
|
+
})
|
|
25887
|
+
.then(function(r) { return r.json().then(function(d) { return { ok: r.ok, data: d }; }); })
|
|
25888
|
+
.then(function(res) {
|
|
25889
|
+
if (btn) btn.disabled = false;
|
|
25890
|
+
if (res.ok && res.data.status === 'ok') {
|
|
25891
|
+
if (statusEl) { statusEl.textContent = 'Saved'; statusEl.className = 'modal-status ok'; }
|
|
25892
|
+
updateContextIndicator();
|
|
25893
|
+
setTimeout(function() { window.closeContextModal(); }, 800);
|
|
25894
|
+
} else {
|
|
25895
|
+
var msg = (res.data && res.data.message) ? res.data.message : 'Failed to save';
|
|
25896
|
+
if (statusEl) { statusEl.textContent = msg; statusEl.className = 'modal-status error'; }
|
|
25897
|
+
}
|
|
25898
|
+
})
|
|
25899
|
+
.catch(function(e) {
|
|
25900
|
+
if (btn) btn.disabled = false;
|
|
25901
|
+
if (statusEl) { statusEl.textContent = String(e); statusEl.className = 'modal-status error'; }
|
|
25902
|
+
});
|
|
25903
|
+
};
|
|
25904
|
+
|
|
25905
|
+
function updateContextIndicator() {
|
|
25906
|
+
authFetch('/api/team/insights/context')
|
|
25907
|
+
.then(function(r) { return r.json(); })
|
|
25908
|
+
.then(function(data) {
|
|
25909
|
+
var el = document.getElementById('context-indicator');
|
|
25910
|
+
if (!el) return;
|
|
25911
|
+
if (data.configured && data.context && data.context.updatedAt) {
|
|
25912
|
+
el.textContent = 'Custom context active \xB7 saved ' + new Date(data.context.updatedAt).toLocaleDateString();
|
|
25913
|
+
} else {
|
|
25914
|
+
el.textContent = '';
|
|
25915
|
+
}
|
|
25916
|
+
})
|
|
25917
|
+
.catch(function() {});
|
|
25918
|
+
}
|
|
25919
|
+
|
|
25920
|
+
function boot() {
|
|
25921
|
+
checkAuth().then(function(ok) {
|
|
25922
|
+
if (!ok) return;
|
|
25923
|
+
startStreaming();
|
|
25924
|
+
updateContextIndicator();
|
|
25925
|
+
});
|
|
25926
|
+
}
|
|
25927
|
+
|
|
24750
25928
|
boot();
|
|
24751
25929
|
})();
|
|
24752
25930
|
</script>
|
|
@@ -25046,6 +26224,97 @@ async function startDashboardServer(options = {}) {
|
|
|
25046
26224
|
const pathname = parsePathname(url);
|
|
25047
26225
|
const segments = parsePathSegments(pathname);
|
|
25048
26226
|
const method = req.method ?? "GET";
|
|
26227
|
+
if (method === "POST" && pathname === "/api/team/budget") {
|
|
26228
|
+
let body = "";
|
|
26229
|
+
req.setEncoding("utf8");
|
|
26230
|
+
req.on("data", (chunk) => {
|
|
26231
|
+
body += chunk;
|
|
26232
|
+
});
|
|
26233
|
+
req.on("end", () => {
|
|
26234
|
+
let parsedBody;
|
|
26235
|
+
try {
|
|
26236
|
+
parsedBody = body.length > 0 ? JSON.parse(body) : {};
|
|
26237
|
+
} catch {
|
|
26238
|
+
sendJson(res, 400, { status: "error", message: "invalid JSON body" });
|
|
26239
|
+
return;
|
|
26240
|
+
}
|
|
26241
|
+
const authHeader = req.headers["authorization"];
|
|
26242
|
+
const headers = { "Content-Type": "application/json" };
|
|
26243
|
+
if (typeof authHeader === "string") {
|
|
26244
|
+
headers["Authorization"] = authHeader;
|
|
26245
|
+
}
|
|
26246
|
+
void fetch(`${apiBaseUrl}/v1/team/budget`, {
|
|
26247
|
+
method: "POST",
|
|
26248
|
+
headers,
|
|
26249
|
+
body: JSON.stringify(parsedBody)
|
|
26250
|
+
}).then(async (apiResponse) => {
|
|
26251
|
+
const payload = await apiResponse.json();
|
|
26252
|
+
sendJson(res, apiResponse.status, payload);
|
|
26253
|
+
}).catch((error) => {
|
|
26254
|
+
sendJson(res, 502, { status: "error", message: `proxy error: ${String(error)}` });
|
|
26255
|
+
});
|
|
26256
|
+
});
|
|
26257
|
+
return;
|
|
26258
|
+
}
|
|
26259
|
+
if (method === "POST" && pathname === "/api/team/insights/context") {
|
|
26260
|
+
let body = "";
|
|
26261
|
+
req.setEncoding("utf8");
|
|
26262
|
+
req.on("data", (chunk) => {
|
|
26263
|
+
body += chunk;
|
|
26264
|
+
});
|
|
26265
|
+
req.on("end", () => {
|
|
26266
|
+
let parsedBody;
|
|
26267
|
+
try {
|
|
26268
|
+
parsedBody = body.length > 0 ? JSON.parse(body) : {};
|
|
26269
|
+
} catch {
|
|
26270
|
+
sendJson(res, 400, { status: "error", message: "invalid JSON body" });
|
|
26271
|
+
return;
|
|
26272
|
+
}
|
|
26273
|
+
const authHeader = req.headers["authorization"];
|
|
26274
|
+
const headers = { "Content-Type": "application/json" };
|
|
26275
|
+
if (typeof authHeader === "string") {
|
|
26276
|
+
headers["Authorization"] = authHeader;
|
|
26277
|
+
}
|
|
26278
|
+
void fetch(`${apiBaseUrl}/v1/team/insights/context`, {
|
|
26279
|
+
method: "POST",
|
|
26280
|
+
headers,
|
|
26281
|
+
body: JSON.stringify(parsedBody)
|
|
26282
|
+
}).then(async (apiResponse) => {
|
|
26283
|
+
const payload = await apiResponse.json();
|
|
26284
|
+
sendJson(res, apiResponse.status, payload);
|
|
26285
|
+
}).catch((error) => {
|
|
26286
|
+
sendJson(res, 502, { status: "error", message: `proxy error: ${String(error)}` });
|
|
26287
|
+
});
|
|
26288
|
+
});
|
|
26289
|
+
return;
|
|
26290
|
+
}
|
|
26291
|
+
if (method === "POST" && pathname === "/api/team/insights/generate") {
|
|
26292
|
+
let body = "";
|
|
26293
|
+
req.setEncoding("utf8");
|
|
26294
|
+
req.on("data", (chunk) => {
|
|
26295
|
+
body += chunk;
|
|
26296
|
+
});
|
|
26297
|
+
req.on("end", () => {
|
|
26298
|
+
const parsedUrl = new URL(url, "http://localhost");
|
|
26299
|
+
const queryString = parsedUrl.search;
|
|
26300
|
+
const authHeader = req.headers["authorization"];
|
|
26301
|
+
const headers = { "Content-Type": "application/json" };
|
|
26302
|
+
if (typeof authHeader === "string") {
|
|
26303
|
+
headers["Authorization"] = authHeader;
|
|
26304
|
+
}
|
|
26305
|
+
void fetch(`${apiBaseUrl}/v1/team/insights/generate${queryString}`, {
|
|
26306
|
+
method: "POST",
|
|
26307
|
+
headers,
|
|
26308
|
+
body: body.length > 0 ? body : "{}"
|
|
26309
|
+
}).then(async (apiResponse) => {
|
|
26310
|
+
const payload = await apiResponse.json();
|
|
26311
|
+
sendJson(res, apiResponse.status, payload);
|
|
26312
|
+
}).catch((error) => {
|
|
26313
|
+
sendJson(res, 502, { status: "error", message: `proxy error: ${String(error)}` });
|
|
26314
|
+
});
|
|
26315
|
+
});
|
|
26316
|
+
return;
|
|
26317
|
+
}
|
|
25049
26318
|
if (method === "POST" && (pathname === "/api/settings/insights" || segments.length === 4 && segments[0] === "api" && segments[1] === "session" && segments[3] === "insights")) {
|
|
25050
26319
|
let body = "";
|
|
25051
26320
|
req.setEncoding("utf8");
|
|
@@ -25129,6 +26398,88 @@ async function startDashboardServer(options = {}) {
|
|
|
25129
26398
|
});
|
|
25130
26399
|
return;
|
|
25131
26400
|
}
|
|
26401
|
+
if (pathname === "/api/auth/check") {
|
|
26402
|
+
const authHeader = req.headers["authorization"];
|
|
26403
|
+
const headers = {};
|
|
26404
|
+
if (typeof authHeader === "string") {
|
|
26405
|
+
headers["Authorization"] = authHeader;
|
|
26406
|
+
}
|
|
26407
|
+
void fetch(`${apiBaseUrl}/v1/auth/check`, { headers }).then(async (apiResponse) => {
|
|
26408
|
+
const payload = await apiResponse.json();
|
|
26409
|
+
sendJson(res, apiResponse.status, payload);
|
|
26410
|
+
}).catch((error) => {
|
|
26411
|
+
sendJson(res, 502, { status: "error", message: `proxy error: ${String(error)}` });
|
|
26412
|
+
});
|
|
26413
|
+
return;
|
|
26414
|
+
}
|
|
26415
|
+
if (pathname.startsWith("/api/team/")) {
|
|
26416
|
+
const authHeader = req.headers["authorization"];
|
|
26417
|
+
const headers = {};
|
|
26418
|
+
if (typeof authHeader === "string") {
|
|
26419
|
+
headers["Authorization"] = authHeader;
|
|
26420
|
+
}
|
|
26421
|
+
const parsedUrl = new URL(url, "http://localhost");
|
|
26422
|
+
const queryString = parsedUrl.search;
|
|
26423
|
+
const teamPath = pathname.replace("/api/team/", "/v1/team/");
|
|
26424
|
+
if (pathname === "/api/team/stream") {
|
|
26425
|
+
res.statusCode = 200;
|
|
26426
|
+
res.setHeader("Content-Type", "text/event-stream; charset=utf-8");
|
|
26427
|
+
res.setHeader("Cache-Control", "no-cache");
|
|
26428
|
+
res.setHeader("Connection", "keep-alive");
|
|
26429
|
+
res.setHeader("X-Accel-Buffering", "no");
|
|
26430
|
+
res.flushHeaders();
|
|
26431
|
+
let closed = false;
|
|
26432
|
+
let writing = false;
|
|
26433
|
+
const writeTeamSnapshot = async () => {
|
|
26434
|
+
if (closed || writing) return;
|
|
26435
|
+
writing = true;
|
|
26436
|
+
try {
|
|
26437
|
+
const [overviewRes, membersRes, costRes, budgetRes] = await Promise.all([
|
|
26438
|
+
fetch(`${apiBaseUrl}/v1/team/overview${queryString}`, { headers }),
|
|
26439
|
+
fetch(`${apiBaseUrl}/v1/team/members${queryString}`, { headers }),
|
|
26440
|
+
fetch(`${apiBaseUrl}/v1/team/cost/daily${queryString}`, { headers }),
|
|
26441
|
+
fetch(`${apiBaseUrl}/v1/team/budget`, { headers })
|
|
26442
|
+
]);
|
|
26443
|
+
const overview = await overviewRes.json();
|
|
26444
|
+
const members = await membersRes.json();
|
|
26445
|
+
const cost = await costRes.json();
|
|
26446
|
+
const budget = await budgetRes.json();
|
|
26447
|
+
const payload = JSON.stringify({ overview, members, cost, budget, emittedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
26448
|
+
res.write("event: team\n");
|
|
26449
|
+
res.write(`data: ${payload}
|
|
26450
|
+
|
|
26451
|
+
`);
|
|
26452
|
+
} catch (error) {
|
|
26453
|
+
res.write("event: bridge_error\n");
|
|
26454
|
+
res.write(`data: ${JSON.stringify({ message: String(error) })}
|
|
26455
|
+
|
|
26456
|
+
`);
|
|
26457
|
+
} finally {
|
|
26458
|
+
writing = false;
|
|
26459
|
+
}
|
|
26460
|
+
};
|
|
26461
|
+
void writeTeamSnapshot();
|
|
26462
|
+
const interval = setInterval(() => {
|
|
26463
|
+
void writeTeamSnapshot();
|
|
26464
|
+
}, 2e3);
|
|
26465
|
+
const cleanup = () => {
|
|
26466
|
+
if (closed) return;
|
|
26467
|
+
closed = true;
|
|
26468
|
+
clearInterval(interval);
|
|
26469
|
+
if (!res.writableEnded) res.end();
|
|
26470
|
+
};
|
|
26471
|
+
req.on("close", cleanup);
|
|
26472
|
+
res.on("close", cleanup);
|
|
26473
|
+
return;
|
|
26474
|
+
}
|
|
26475
|
+
void fetch(`${apiBaseUrl}${teamPath}${queryString}`, { headers }).then(async (apiResponse) => {
|
|
26476
|
+
const payload = await apiResponse.json();
|
|
26477
|
+
sendJson(res, apiResponse.status, payload);
|
|
26478
|
+
}).catch((error) => {
|
|
26479
|
+
sendJson(res, 502, { status: "error", message: `proxy error: ${String(error)}` });
|
|
26480
|
+
});
|
|
26481
|
+
return;
|
|
26482
|
+
}
|
|
25132
26483
|
if (segments.length === 3 && segments[0] === "api" && segments[1] === "session") {
|
|
25133
26484
|
const encodedSessionId = segments[2];
|
|
25134
26485
|
let sessionId = "";
|
|
@@ -25181,7 +26532,7 @@ async function startDashboardServer(options = {}) {
|
|
|
25181
26532
|
return;
|
|
25182
26533
|
}
|
|
25183
26534
|
if (pathname === "/") {
|
|
25184
|
-
sendHtml(res, 200, renderDashboardHtml());
|
|
26535
|
+
sendHtml(res, 200, renderDashboardHtml(options.currentUserEmail !== void 0 ? { currentUserEmail: options.currentUserEmail } : {}));
|
|
25185
26536
|
return;
|
|
25186
26537
|
}
|
|
25187
26538
|
sendJson(res, 404, {
|
|
@@ -25341,6 +26692,7 @@ var SqliteClient = class {
|
|
|
25341
26692
|
this.migrateDeduplicateEvents();
|
|
25342
26693
|
this.db.exec(SCHEMA_SQL);
|
|
25343
26694
|
this.migrateRebuildBrokenTraces();
|
|
26695
|
+
this.migrateTeamColumns();
|
|
25344
26696
|
}
|
|
25345
26697
|
async insertJsonEachRow(request) {
|
|
25346
26698
|
if (request.rows.length === 0) return;
|
|
@@ -25352,18 +26704,20 @@ var SqliteClient = class {
|
|
|
25352
26704
|
if (rows.length === 0) return;
|
|
25353
26705
|
const upsert = this.db.prepare(`
|
|
25354
26706
|
INSERT INTO session_traces
|
|
25355
|
-
(session_id, version, started_at, ended_at, user_id, git_repo, git_branch,
|
|
26707
|
+
(session_id, version, started_at, ended_at, user_id, user_email, user_display_name, git_repo, git_branch,
|
|
25356
26708
|
prompt_count, tool_call_count, api_call_count, total_cost_usd,
|
|
25357
26709
|
total_input_tokens, total_output_tokens, total_cache_read_tokens, total_cache_write_tokens,
|
|
25358
26710
|
lines_added, lines_removed,
|
|
25359
26711
|
models_used, tools_used, files_touched, commit_count, updated_at)
|
|
25360
26712
|
VALUES
|
|
25361
|
-
(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
26713
|
+
(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
25362
26714
|
ON CONFLICT(session_id) DO UPDATE SET
|
|
25363
26715
|
version = excluded.version,
|
|
25364
26716
|
started_at = excluded.started_at,
|
|
25365
26717
|
ended_at = excluded.ended_at,
|
|
25366
26718
|
user_id = excluded.user_id,
|
|
26719
|
+
user_email = COALESCE(excluded.user_email, session_traces.user_email),
|
|
26720
|
+
user_display_name = COALESCE(excluded.user_display_name, session_traces.user_display_name),
|
|
25367
26721
|
git_repo = excluded.git_repo,
|
|
25368
26722
|
git_branch = excluded.git_branch,
|
|
25369
26723
|
prompt_count = excluded.prompt_count,
|
|
@@ -25390,6 +26744,8 @@ var SqliteClient = class {
|
|
|
25390
26744
|
row.started_at,
|
|
25391
26745
|
row.ended_at,
|
|
25392
26746
|
row.user_id,
|
|
26747
|
+
row.user_email ?? null,
|
|
26748
|
+
row.user_display_name ?? null,
|
|
25393
26749
|
row.git_repo,
|
|
25394
26750
|
row.git_branch,
|
|
25395
26751
|
row.prompt_count,
|
|
@@ -25611,6 +26967,32 @@ var SqliteClient = class {
|
|
|
25611
26967
|
updated_at = excluded.updated_at
|
|
25612
26968
|
`).run(key, value);
|
|
25613
26969
|
}
|
|
26970
|
+
getTeamBudget() {
|
|
26971
|
+
const row = this.db.prepare(
|
|
26972
|
+
"SELECT monthly_limit_usd, alert_threshold_percent FROM team_budgets WHERE id = 1"
|
|
26973
|
+
).get();
|
|
26974
|
+
if (row === void 0) return void 0;
|
|
26975
|
+
return {
|
|
26976
|
+
monthlyLimitUsd: row.monthly_limit_usd,
|
|
26977
|
+
alertThresholdPercent: row.alert_threshold_percent
|
|
26978
|
+
};
|
|
26979
|
+
}
|
|
26980
|
+
upsertTeamBudget(limitUsd, alertPercent) {
|
|
26981
|
+
this.db.prepare(`
|
|
26982
|
+
INSERT INTO team_budgets (id, monthly_limit_usd, alert_threshold_percent, updated_at)
|
|
26983
|
+
VALUES (1, ?, ?, datetime('now'))
|
|
26984
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
26985
|
+
monthly_limit_usd = excluded.monthly_limit_usd,
|
|
26986
|
+
alert_threshold_percent = excluded.alert_threshold_percent,
|
|
26987
|
+
updated_at = excluded.updated_at
|
|
26988
|
+
`).run(limitUsd, alertPercent);
|
|
26989
|
+
}
|
|
26990
|
+
getMonthSpend(yearMonth) {
|
|
26991
|
+
const row = this.db.prepare(
|
|
26992
|
+
"SELECT COALESCE(SUM(total_cost_usd), 0) AS spend FROM session_traces WHERE substr(started_at, 1, 7) = ?"
|
|
26993
|
+
).get(yearMonth);
|
|
26994
|
+
return row.spend;
|
|
26995
|
+
}
|
|
25614
26996
|
close() {
|
|
25615
26997
|
this.db.close();
|
|
25616
26998
|
}
|
|
@@ -25742,6 +27124,42 @@ var SqliteClient = class {
|
|
|
25742
27124
|
console.log("[agent-trace] migrating: rebuilding session traces with correct models/tools");
|
|
25743
27125
|
this.rebuildSessionTracesFromEvents();
|
|
25744
27126
|
}
|
|
27127
|
+
/**
|
|
27128
|
+
* Migration: add team-related columns and tables.
|
|
27129
|
+
*/
|
|
27130
|
+
migrateTeamColumns() {
|
|
27131
|
+
const addColumnIfMissing = (table, column, definition) => {
|
|
27132
|
+
const cols = this.db.prepare(`PRAGMA table_info('${table}')`).all();
|
|
27133
|
+
if (!cols.some((c) => c.name === column)) {
|
|
27134
|
+
this.db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${definition}`);
|
|
27135
|
+
}
|
|
27136
|
+
};
|
|
27137
|
+
const tracesExist = this.db.prepare(
|
|
27138
|
+
"SELECT 1 FROM sqlite_master WHERE type='table' AND name='session_traces'"
|
|
27139
|
+
).get();
|
|
27140
|
+
if (tracesExist !== void 0) {
|
|
27141
|
+
addColumnIfMissing("session_traces", "user_email", "TEXT");
|
|
27142
|
+
addColumnIfMissing("session_traces", "user_display_name", "TEXT");
|
|
27143
|
+
}
|
|
27144
|
+
const eventsExist = this.db.prepare(
|
|
27145
|
+
"SELECT 1 FROM sqlite_master WHERE type='table' AND name='agent_events'"
|
|
27146
|
+
).get();
|
|
27147
|
+
if (eventsExist !== void 0) {
|
|
27148
|
+
addColumnIfMissing("agent_events", "user_email", "TEXT");
|
|
27149
|
+
}
|
|
27150
|
+
this.db.exec(`
|
|
27151
|
+
CREATE TABLE IF NOT EXISTS team_budgets (
|
|
27152
|
+
id INTEGER PRIMARY KEY DEFAULT 1,
|
|
27153
|
+
monthly_limit_usd REAL NOT NULL,
|
|
27154
|
+
alert_threshold_percent REAL NOT NULL DEFAULT 80,
|
|
27155
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
27156
|
+
)
|
|
27157
|
+
`);
|
|
27158
|
+
this.db.exec(`
|
|
27159
|
+
CREATE INDEX IF NOT EXISTS idx_traces_started_at ON session_traces(started_at);
|
|
27160
|
+
CREATE INDEX IF NOT EXISTS idx_traces_user_id ON session_traces(user_id)
|
|
27161
|
+
`);
|
|
27162
|
+
}
|
|
25745
27163
|
/**
|
|
25746
27164
|
* Rebuild session_traces by aggregating deduplicated agent_events.
|
|
25747
27165
|
* Called after dedup migration so the dashboard has correct metrics immediately.
|
|
@@ -26715,7 +28133,7 @@ function createAnthropicProvider(apiKey, model) {
|
|
|
26715
28133
|
return {
|
|
26716
28134
|
provider: "anthropic",
|
|
26717
28135
|
model,
|
|
26718
|
-
async complete(system, user) {
|
|
28136
|
+
async complete(system, user, maxTokens) {
|
|
26719
28137
|
const response = await fetch(endpoint, {
|
|
26720
28138
|
method: "POST",
|
|
26721
28139
|
headers: {
|
|
@@ -26725,7 +28143,7 @@ function createAnthropicProvider(apiKey, model) {
|
|
|
26725
28143
|
},
|
|
26726
28144
|
body: JSON.stringify({
|
|
26727
28145
|
model,
|
|
26728
|
-
max_tokens: 1024,
|
|
28146
|
+
max_tokens: maxTokens ?? 1024,
|
|
26729
28147
|
system,
|
|
26730
28148
|
messages: [{ role: "user", content: user }]
|
|
26731
28149
|
})
|
|
@@ -26763,7 +28181,7 @@ function createOpenAiProvider(apiKey, model) {
|
|
|
26763
28181
|
return {
|
|
26764
28182
|
provider: "openai",
|
|
26765
28183
|
model,
|
|
26766
|
-
async complete(system, user) {
|
|
28184
|
+
async complete(system, user, maxTokens) {
|
|
26767
28185
|
const response = await fetch(endpoint, {
|
|
26768
28186
|
method: "POST",
|
|
26769
28187
|
headers: {
|
|
@@ -26772,7 +28190,7 @@ function createOpenAiProvider(apiKey, model) {
|
|
|
26772
28190
|
},
|
|
26773
28191
|
body: JSON.stringify({
|
|
26774
28192
|
model,
|
|
26775
|
-
max_tokens: 1024,
|
|
28193
|
+
max_tokens: maxTokens ?? 1024,
|
|
26776
28194
|
messages: [
|
|
26777
28195
|
{ role: "system", content: system },
|
|
26778
28196
|
{ role: "user", content: user }
|
|
@@ -26811,7 +28229,7 @@ function createGeminiProvider(apiKey, model) {
|
|
|
26811
28229
|
return {
|
|
26812
28230
|
provider: "gemini",
|
|
26813
28231
|
model,
|
|
26814
|
-
async complete(system, user) {
|
|
28232
|
+
async complete(system, user, maxTokens) {
|
|
26815
28233
|
const url = `${baseUrl}/models/${model}:generateContent?key=${apiKey}`;
|
|
26816
28234
|
const response = await fetch(url, {
|
|
26817
28235
|
method: "POST",
|
|
@@ -26819,7 +28237,7 @@ function createGeminiProvider(apiKey, model) {
|
|
|
26819
28237
|
body: JSON.stringify({
|
|
26820
28238
|
systemInstruction: { parts: [{ text: system }] },
|
|
26821
28239
|
contents: [{ parts: [{ text: user }] }],
|
|
26822
|
-
generationConfig: { maxOutputTokens: 1024 }
|
|
28240
|
+
generationConfig: { maxOutputTokens: maxTokens ?? 1024 }
|
|
26823
28241
|
})
|
|
26824
28242
|
});
|
|
26825
28243
|
if (!response.ok) {
|
|
@@ -26851,7 +28269,7 @@ function createOpenRouterProvider(apiKey, model) {
|
|
|
26851
28269
|
return {
|
|
26852
28270
|
provider: "openrouter",
|
|
26853
28271
|
model,
|
|
26854
|
-
async complete(system, user) {
|
|
28272
|
+
async complete(system, user, maxTokens) {
|
|
26855
28273
|
const response = await fetch(endpoint, {
|
|
26856
28274
|
method: "POST",
|
|
26857
28275
|
headers: {
|
|
@@ -26860,7 +28278,7 @@ function createOpenRouterProvider(apiKey, model) {
|
|
|
26860
28278
|
},
|
|
26861
28279
|
body: JSON.stringify({
|
|
26862
28280
|
model,
|
|
26863
|
-
max_tokens: 1024,
|
|
28281
|
+
max_tokens: maxTokens ?? 1024,
|
|
26864
28282
|
messages: [
|
|
26865
28283
|
{ role: "system", content: system },
|
|
26866
28284
|
{ role: "user", content: user }
|
|
@@ -27037,8 +28455,239 @@ async function generateSessionInsight(trace, provider) {
|
|
|
27037
28455
|
return result;
|
|
27038
28456
|
}
|
|
27039
28457
|
|
|
28458
|
+
// packages/api/src/team-insights-generator.ts
|
|
28459
|
+
var TEAM_SYSTEM_PROMPT = `You are the AI analytics engine for agent-trace, a developer tool that tracks AI coding agent usage across engineering teams. A manager is viewing their team dashboard and has requested your analysis.
|
|
28460
|
+
|
|
28461
|
+
<role>
|
|
28462
|
+
You are a data analyst who turns raw telemetry into actionable management insights. You write like a sharp, senior engineering director \u2014 concise, evidence-based, zero fluff. Every sentence must reference a specific number from the data.
|
|
28463
|
+
</role>
|
|
28464
|
+
|
|
28465
|
+
<output_format>
|
|
28466
|
+
Respond with ONLY a single JSON object (no markdown fences, no commentary before or after). The JSON must conform exactly to this schema:
|
|
28467
|
+
|
|
28468
|
+
{"executiveSummary":"string","costAnalysis":"string","productivityAnalysis":"string","memberHighlights":[{"userId":"string","displayName":"string or null","strength":"string","concern":"string or null","recommendation":"string"}],"risks":["string"],"recommendations":["string"],"forecast":"string or null"}
|
|
28469
|
+
</output_format>
|
|
28470
|
+
|
|
28471
|
+
<field_guidelines>
|
|
28472
|
+
executiveSummary: 3-4 sentences. Open with the single most important finding. Include total spend, member count, commit count, and the key efficiency metric. End with overall team health assessment.
|
|
28473
|
+
|
|
28474
|
+
costAnalysis: 2-3 sentences. Rank members by cost-efficiency ($/commit). Call out the best and worst. If someone has high spend with zero or few commits, flag it directly. Compare individual $/commit to team average.
|
|
28475
|
+
|
|
28476
|
+
productivityAnalysis: 2-3 sentences. Analyze commit velocity, lines of code per session, and tool diversity. Note which tools correlate with higher output. Identify if anyone is underutilizing available tools.
|
|
28477
|
+
|
|
28478
|
+
memberHighlights: One entry per member in the data. userId must exactly match the data.
|
|
28479
|
+
- strength: Reference their specific numbers. What do they do well relative to the team?
|
|
28480
|
+
- concern: Reference specific numbers. Set to null only if genuinely nothing is concerning. Low commits, high cost, late-night patterns, or low tool diversity are all valid concerns.
|
|
28481
|
+
- recommendation: One specific, actionable suggestion. "Try model X to reduce cost" or "Pair with alice on tool Y adoption."
|
|
28482
|
+
|
|
28483
|
+
risks: 1-3 items. Only risks supported by the data. Examples: budget trajectory, workload imbalance (compare session counts), burnout signals (late-night activity), single-model dependency, low commit rates.
|
|
28484
|
+
|
|
28485
|
+
recommendations: 3-5 items. Concrete and actionable. Reference specific members, models, or tools. Examples: "Switch bob from claude-opus to claude-sonnet to save ~40% on cost", "Rebalance: dave has 4x the sessions of carol."
|
|
28486
|
+
|
|
28487
|
+
forecast: If daily cost data has 3+ days, extrapolate monthly spend. Otherwise null.
|
|
28488
|
+
</field_guidelines>
|
|
28489
|
+
|
|
28490
|
+
<constraints>
|
|
28491
|
+
- Do NOT wrap the JSON in markdown code fences
|
|
28492
|
+
- Do NOT include any text before or after the JSON
|
|
28493
|
+
- Every claim must cite a number from the provided data
|
|
28494
|
+
- Keep each string field under 500 characters
|
|
28495
|
+
- The memberHighlights array must include ALL members present in the data
|
|
28496
|
+
- Be constructive: frame concerns as improvement opportunities
|
|
28497
|
+
</constraints>`;
|
|
28498
|
+
function buildTeamAnalyticsPrompt(traces, from, to) {
|
|
28499
|
+
const sections = [];
|
|
28500
|
+
const memberMap = /* @__PURE__ */ new Map();
|
|
28501
|
+
let totalCost = 0;
|
|
28502
|
+
let totalCommits = 0;
|
|
28503
|
+
let totalPRs = 0;
|
|
28504
|
+
let totalLinesAdded = 0;
|
|
28505
|
+
let totalLinesRemoved = 0;
|
|
28506
|
+
let totalSessions = 0;
|
|
28507
|
+
for (const trace of traces) {
|
|
28508
|
+
const userId = trace.user.id;
|
|
28509
|
+
if (userId === "unknown_user") continue;
|
|
28510
|
+
const existing = memberMap.get(userId) ?? {
|
|
28511
|
+
userId,
|
|
28512
|
+
displayName: trace.user.displayName ?? null,
|
|
28513
|
+
sessionCount: 0,
|
|
28514
|
+
totalCostUsd: 0,
|
|
28515
|
+
commitCount: 0,
|
|
28516
|
+
prCount: 0,
|
|
28517
|
+
linesAdded: 0,
|
|
28518
|
+
linesRemoved: 0,
|
|
28519
|
+
totalInputTokens: 0,
|
|
28520
|
+
totalOutputTokens: 0,
|
|
28521
|
+
models: [],
|
|
28522
|
+
tools: [],
|
|
28523
|
+
repos: [],
|
|
28524
|
+
peakHours: [],
|
|
28525
|
+
avgSessionCostUsd: 0,
|
|
28526
|
+
costPerCommit: 0
|
|
28527
|
+
};
|
|
28528
|
+
existing.sessionCount += 1;
|
|
28529
|
+
existing.totalCostUsd += trace.metrics.totalCostUsd;
|
|
28530
|
+
existing.commitCount += trace.git.commits.length;
|
|
28531
|
+
existing.prCount += trace.git.pullRequests.length;
|
|
28532
|
+
existing.linesAdded += trace.metrics.linesAdded;
|
|
28533
|
+
existing.linesRemoved += trace.metrics.linesRemoved;
|
|
28534
|
+
existing.totalInputTokens += trace.metrics.totalInputTokens;
|
|
28535
|
+
existing.totalOutputTokens += trace.metrics.totalOutputTokens;
|
|
28536
|
+
if (trace.user.displayName !== void 0) {
|
|
28537
|
+
existing.displayName = trace.user.displayName;
|
|
28538
|
+
}
|
|
28539
|
+
for (const model of trace.metrics.modelsUsed) {
|
|
28540
|
+
if (!existing.models.includes(model)) existing.models.push(model);
|
|
28541
|
+
}
|
|
28542
|
+
for (const tool of trace.metrics.toolsUsed) {
|
|
28543
|
+
if (!existing.tools.includes(tool)) existing.tools.push(tool);
|
|
28544
|
+
}
|
|
28545
|
+
const repo = trace.environment.gitRepo;
|
|
28546
|
+
if (repo !== void 0 && repo.length > 0 && !existing.repos.includes(repo)) {
|
|
28547
|
+
existing.repos.push(repo);
|
|
28548
|
+
}
|
|
28549
|
+
const hour = new Date(trace.startedAt).getUTCHours();
|
|
28550
|
+
existing.peakHours.push(hour);
|
|
28551
|
+
totalCost += trace.metrics.totalCostUsd;
|
|
28552
|
+
totalCommits += trace.git.commits.length;
|
|
28553
|
+
totalPRs += trace.git.pullRequests.length;
|
|
28554
|
+
totalLinesAdded += trace.metrics.linesAdded;
|
|
28555
|
+
totalLinesRemoved += trace.metrics.linesRemoved;
|
|
28556
|
+
totalSessions += 1;
|
|
28557
|
+
memberMap.set(userId, existing);
|
|
28558
|
+
}
|
|
28559
|
+
sections.push(`<team_data>`);
|
|
28560
|
+
sections.push(`<period from="${from}" to="${to}" />`);
|
|
28561
|
+
sections.push(`<summary members="${String(memberMap.size)}" sessions="${String(totalSessions)}" total_cost_usd="${totalCost.toFixed(2)}" total_commits="${String(totalCommits)}" total_prs="${String(totalPRs)}" lines_added="${String(totalLinesAdded)}" lines_removed="${String(totalLinesRemoved)}" avg_cost_per_session="${totalSessions > 0 ? (totalCost / totalSessions).toFixed(4) : "0"}" avg_cost_per_commit="${totalCommits > 0 ? (totalCost / totalCommits).toFixed(2) : "N/A"}" />`);
|
|
28562
|
+
const members = [...memberMap.values()].sort((a, b) => b.totalCostUsd - a.totalCostUsd);
|
|
28563
|
+
for (const m of members) {
|
|
28564
|
+
m.avgSessionCostUsd = m.sessionCount > 0 ? m.totalCostUsd / m.sessionCount : 0;
|
|
28565
|
+
m.costPerCommit = m.commitCount > 0 ? m.totalCostUsd / m.commitCount : 0;
|
|
28566
|
+
const hourCounts = new Array(24).fill(0);
|
|
28567
|
+
for (const h of m.peakHours) hourCounts[h] = (hourCounts[h] ?? 0) + 1;
|
|
28568
|
+
const peakHour = hourCounts.indexOf(Math.max(...hourCounts));
|
|
28569
|
+
const lateNight = m.peakHours.filter((h) => h >= 22 || h <= 5).length;
|
|
28570
|
+
sections.push(`<member id="${m.userId}" display_name="${m.displayName ?? "none"}">`);
|
|
28571
|
+
sections.push(` sessions="${String(m.sessionCount)}" cost_usd="${m.totalCostUsd.toFixed(2)}" avg_session_cost="${m.avgSessionCostUsd.toFixed(4)}"`);
|
|
28572
|
+
sections.push(` commits="${String(m.commitCount)}" prs="${String(m.prCount)}" cost_per_commit="${m.commitCount > 0 ? m.costPerCommit.toFixed(2) : "N/A (0 commits)"}"`);
|
|
28573
|
+
sections.push(` lines_added="${String(m.linesAdded)}" lines_removed="${String(m.linesRemoved)}"`);
|
|
28574
|
+
sections.push(` input_tokens="${String(m.totalInputTokens)}" output_tokens="${String(m.totalOutputTokens)}"`);
|
|
28575
|
+
sections.push(` models="${m.models.join(", ")}"`);
|
|
28576
|
+
sections.push(` tools="${m.tools.slice(0, 12).join(", ")}${m.tools.length > 12 ? " +" + String(m.tools.length - 12) + " more" : ""}"`);
|
|
28577
|
+
sections.push(` repos="${m.repos.join(", ")}"`);
|
|
28578
|
+
sections.push(` peak_hour_utc="${String(peakHour)}" late_night_sessions="${String(lateNight)}"`);
|
|
28579
|
+
sections.push(`</member>`);
|
|
28580
|
+
}
|
|
28581
|
+
const dailyCost = /* @__PURE__ */ new Map();
|
|
28582
|
+
for (const trace of traces) {
|
|
28583
|
+
if (trace.user.id === "unknown_user") continue;
|
|
28584
|
+
const date = trace.startedAt.slice(0, 10);
|
|
28585
|
+
dailyCost.set(date, (dailyCost.get(date) ?? 0) + trace.metrics.totalCostUsd);
|
|
28586
|
+
}
|
|
28587
|
+
const sortedDays = [...dailyCost.entries()].sort(([a], [b]) => a.localeCompare(b));
|
|
28588
|
+
if (sortedDays.length > 0) {
|
|
28589
|
+
sections.push(`<daily_cost_trend>`);
|
|
28590
|
+
for (const [date, cost] of sortedDays.slice(-14)) {
|
|
28591
|
+
sections.push(` <day date="${date}" cost_usd="${cost.toFixed(2)}" />`);
|
|
28592
|
+
}
|
|
28593
|
+
sections.push(`</daily_cost_trend>`);
|
|
28594
|
+
}
|
|
28595
|
+
sections.push(`</team_data>`);
|
|
28596
|
+
sections.push(``);
|
|
28597
|
+
sections.push(`Analyze this team's AI coding agent usage. Respond with only the JSON object, no other text.`);
|
|
28598
|
+
return sections.join("\n");
|
|
28599
|
+
}
|
|
28600
|
+
function parseTeamInsightJson(raw) {
|
|
28601
|
+
let jsonStr = raw.trim();
|
|
28602
|
+
const fenceMatch = jsonStr.match(/```(?:json)?\s*\n?([\s\S]*?)\n?\s*```/);
|
|
28603
|
+
if (fenceMatch !== null && fenceMatch[1] !== void 0) {
|
|
28604
|
+
jsonStr = fenceMatch[1].trim();
|
|
28605
|
+
}
|
|
28606
|
+
if (!jsonStr.startsWith("{")) {
|
|
28607
|
+
const braceStart = jsonStr.indexOf("{");
|
|
28608
|
+
const braceEnd = jsonStr.lastIndexOf("}");
|
|
28609
|
+
if (braceStart >= 0 && braceEnd > braceStart) {
|
|
28610
|
+
jsonStr = jsonStr.slice(braceStart, braceEnd + 1);
|
|
28611
|
+
}
|
|
28612
|
+
}
|
|
28613
|
+
try {
|
|
28614
|
+
const parsed = JSON.parse(jsonStr);
|
|
28615
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return void 0;
|
|
28616
|
+
const obj = parsed;
|
|
28617
|
+
const executiveSummary = typeof obj["executiveSummary"] === "string" ? obj["executiveSummary"] : "";
|
|
28618
|
+
if (executiveSummary.length === 0) return void 0;
|
|
28619
|
+
const costAnalysis = typeof obj["costAnalysis"] === "string" ? obj["costAnalysis"] : "";
|
|
28620
|
+
const productivityAnalysis = typeof obj["productivityAnalysis"] === "string" ? obj["productivityAnalysis"] : "";
|
|
28621
|
+
const rawMembers = Array.isArray(obj["memberHighlights"]) ? obj["memberHighlights"] : [];
|
|
28622
|
+
const memberHighlights = rawMembers.filter((m) => typeof m === "object" && m !== null).map((m) => ({
|
|
28623
|
+
userId: typeof m["userId"] === "string" ? m["userId"] : "",
|
|
28624
|
+
displayName: typeof m["displayName"] === "string" ? m["displayName"] : null,
|
|
28625
|
+
strength: typeof m["strength"] === "string" ? m["strength"] : "",
|
|
28626
|
+
concern: typeof m["concern"] === "string" && m["concern"].length > 0 ? m["concern"] : null,
|
|
28627
|
+
recommendation: typeof m["recommendation"] === "string" ? m["recommendation"] : ""
|
|
28628
|
+
})).filter((m) => m.userId.length > 0);
|
|
28629
|
+
const risks = Array.isArray(obj["risks"]) ? obj["risks"].filter((r) => typeof r === "string") : [];
|
|
28630
|
+
const recommendations = Array.isArray(obj["recommendations"]) ? obj["recommendations"].filter((r) => typeof r === "string") : [];
|
|
28631
|
+
const forecast = typeof obj["forecast"] === "string" && obj["forecast"].length > 0 ? obj["forecast"] : null;
|
|
28632
|
+
return {
|
|
28633
|
+
executiveSummary,
|
|
28634
|
+
costAnalysis,
|
|
28635
|
+
productivityAnalysis,
|
|
28636
|
+
memberHighlights,
|
|
28637
|
+
risks,
|
|
28638
|
+
recommendations,
|
|
28639
|
+
forecast
|
|
28640
|
+
};
|
|
28641
|
+
} catch {
|
|
28642
|
+
return void 0;
|
|
28643
|
+
}
|
|
28644
|
+
}
|
|
28645
|
+
async function generateTeamInsight(traces, from, to, provider, context) {
|
|
28646
|
+
const userPrompt = buildTeamAnalyticsPrompt(traces, from, to);
|
|
28647
|
+
let systemPrompt = TEAM_SYSTEM_PROMPT;
|
|
28648
|
+
if (context !== void 0) {
|
|
28649
|
+
if (context.companyContext.length > 0) {
|
|
28650
|
+
systemPrompt += `
|
|
28651
|
+
|
|
28652
|
+
<company_context>
|
|
28653
|
+
${context.companyContext}
|
|
28654
|
+
</company_context>`;
|
|
28655
|
+
}
|
|
28656
|
+
if (context.analysisGuidelines.length > 0) {
|
|
28657
|
+
systemPrompt += `
|
|
28658
|
+
|
|
28659
|
+
<manager_guidelines>
|
|
28660
|
+
${context.analysisGuidelines}
|
|
28661
|
+
</manager_guidelines>`;
|
|
28662
|
+
}
|
|
28663
|
+
}
|
|
28664
|
+
const rawResponse = await provider.complete(systemPrompt, userPrompt, 4096);
|
|
28665
|
+
const parsed = parseTeamInsightJson(rawResponse);
|
|
28666
|
+
if (parsed === void 0) {
|
|
28667
|
+
return {
|
|
28668
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
28669
|
+
provider: provider.provider,
|
|
28670
|
+
model: provider.model,
|
|
28671
|
+
executiveSummary: rawResponse.slice(0, 800) || "Failed to generate structured team insight.",
|
|
28672
|
+
costAnalysis: "",
|
|
28673
|
+
productivityAnalysis: "",
|
|
28674
|
+
memberHighlights: [],
|
|
28675
|
+
risks: [],
|
|
28676
|
+
recommendations: [],
|
|
28677
|
+
forecast: null
|
|
28678
|
+
};
|
|
28679
|
+
}
|
|
28680
|
+
return {
|
|
28681
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
28682
|
+
provider: provider.provider,
|
|
28683
|
+
model: provider.model,
|
|
28684
|
+
...parsed
|
|
28685
|
+
};
|
|
28686
|
+
}
|
|
28687
|
+
|
|
27040
28688
|
// packages/api/src/insights-handler.ts
|
|
27041
28689
|
var insightsCache = /* @__PURE__ */ new Map();
|
|
28690
|
+
var teamInsightCache;
|
|
27042
28691
|
var VALID_PROVIDERS = ["anthropic", "openai", "gemini", "openrouter"];
|
|
27043
28692
|
function isValidProvider(value) {
|
|
27044
28693
|
return typeof value === "string" && VALID_PROVIDERS.includes(value);
|
|
@@ -27159,12 +28808,117 @@ async function handlePostSessionInsight(sessionId, dependencies) {
|
|
|
27159
28808
|
payload: { status: "ok", insight }
|
|
27160
28809
|
};
|
|
27161
28810
|
}
|
|
28811
|
+
async function handlePostTeamInsight(searchParams, dependencies) {
|
|
28812
|
+
const accessor = dependencies.insightsConfigAccessor;
|
|
28813
|
+
if (accessor === void 0) {
|
|
28814
|
+
return {
|
|
28815
|
+
statusCode: 400,
|
|
28816
|
+
payload: { status: "error", message: "insights not configured" }
|
|
28817
|
+
};
|
|
28818
|
+
}
|
|
28819
|
+
const config = accessor.getConfig();
|
|
28820
|
+
if (config === void 0) {
|
|
28821
|
+
return {
|
|
28822
|
+
statusCode: 400,
|
|
28823
|
+
payload: { status: "error", message: "no AI provider configured. open settings to add your API key." }
|
|
28824
|
+
};
|
|
28825
|
+
}
|
|
28826
|
+
const forceRegenerate = searchParams.get("force") === "true";
|
|
28827
|
+
if (!forceRegenerate && teamInsightCache !== void 0) {
|
|
28828
|
+
const age = Date.now() - Date.parse(teamInsightCache.generatedAt);
|
|
28829
|
+
if (age < 5 * 60 * 1e3) {
|
|
28830
|
+
return {
|
|
28831
|
+
statusCode: 200,
|
|
28832
|
+
payload: { status: "ok", insight: teamInsightCache }
|
|
28833
|
+
};
|
|
28834
|
+
}
|
|
28835
|
+
}
|
|
28836
|
+
const fromParam = searchParams.get("from");
|
|
28837
|
+
const toParam = searchParams.get("to");
|
|
28838
|
+
const now = /* @__PURE__ */ new Date();
|
|
28839
|
+
const year = now.getFullYear();
|
|
28840
|
+
const month = String(now.getMonth() + 1).padStart(2, "0");
|
|
28841
|
+
const lastDay = new Date(year, now.getMonth() + 1, 0).getDate();
|
|
28842
|
+
const from = fromParam !== null && fromParam.length === 10 ? fromParam : `${year}-${month}-01`;
|
|
28843
|
+
const to = toParam !== null && toParam.length === 10 ? toParam : `${year}-${month}-${String(lastDay).padStart(2, "0")}`;
|
|
28844
|
+
const filters = { from, to };
|
|
28845
|
+
const traces = dependencies.repository.list(filters);
|
|
28846
|
+
if (traces.length === 0) {
|
|
28847
|
+
return {
|
|
28848
|
+
statusCode: 400,
|
|
28849
|
+
payload: { status: "error", message: "no session data available for the selected period" }
|
|
28850
|
+
};
|
|
28851
|
+
}
|
|
28852
|
+
const provider = createLlmProvider(config);
|
|
28853
|
+
const teamContext = accessor.getTeamInsightsContext();
|
|
28854
|
+
const insight = await generateTeamInsight(traces, from, to, provider, teamContext);
|
|
28855
|
+
teamInsightCache = insight;
|
|
28856
|
+
return {
|
|
28857
|
+
statusCode: 200,
|
|
28858
|
+
payload: { status: "ok", insight }
|
|
28859
|
+
};
|
|
28860
|
+
}
|
|
28861
|
+
function handleGetTeamInsightsContext(dependencies) {
|
|
28862
|
+
const accessor = dependencies.insightsConfigAccessor;
|
|
28863
|
+
if (accessor === void 0) {
|
|
28864
|
+
return {
|
|
28865
|
+
statusCode: 200,
|
|
28866
|
+
payload: { status: "ok", configured: false }
|
|
28867
|
+
};
|
|
28868
|
+
}
|
|
28869
|
+
const context = accessor.getTeamInsightsContext();
|
|
28870
|
+
if (context === void 0) {
|
|
28871
|
+
return {
|
|
28872
|
+
statusCode: 200,
|
|
28873
|
+
payload: { status: "ok", configured: false }
|
|
28874
|
+
};
|
|
28875
|
+
}
|
|
28876
|
+
return {
|
|
28877
|
+
statusCode: 200,
|
|
28878
|
+
payload: { status: "ok", configured: true, context }
|
|
28879
|
+
};
|
|
28880
|
+
}
|
|
28881
|
+
function handlePostTeamInsightsContext(body, dependencies) {
|
|
28882
|
+
const accessor = dependencies.insightsConfigAccessor;
|
|
28883
|
+
if (accessor === void 0) {
|
|
28884
|
+
return {
|
|
28885
|
+
statusCode: 500,
|
|
28886
|
+
payload: { status: "error", message: "insights settings not available" }
|
|
28887
|
+
};
|
|
28888
|
+
}
|
|
28889
|
+
if (typeof body !== "object" || body === null || Array.isArray(body)) {
|
|
28890
|
+
return {
|
|
28891
|
+
statusCode: 400,
|
|
28892
|
+
payload: { status: "error", message: "request body must be a JSON object" }
|
|
28893
|
+
};
|
|
28894
|
+
}
|
|
28895
|
+
const record = body;
|
|
28896
|
+
const companyContext = typeof record["companyContext"] === "string" ? record["companyContext"] : "";
|
|
28897
|
+
const analysisGuidelines = typeof record["analysisGuidelines"] === "string" ? record["analysisGuidelines"] : "";
|
|
28898
|
+
if (companyContext.length === 0 && analysisGuidelines.length === 0) {
|
|
28899
|
+
return {
|
|
28900
|
+
statusCode: 400,
|
|
28901
|
+
payload: { status: "error", message: "at least one of companyContext or analysisGuidelines is required" }
|
|
28902
|
+
};
|
|
28903
|
+
}
|
|
28904
|
+
const context = {
|
|
28905
|
+
companyContext,
|
|
28906
|
+
analysisGuidelines,
|
|
28907
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
28908
|
+
};
|
|
28909
|
+
accessor.setTeamInsightsContext(context);
|
|
28910
|
+
return {
|
|
28911
|
+
statusCode: 200,
|
|
28912
|
+
payload: { status: "ok" }
|
|
28913
|
+
};
|
|
28914
|
+
}
|
|
27162
28915
|
|
|
27163
28916
|
// packages/api/src/mapper.ts
|
|
27164
28917
|
function toSessionSummary(trace) {
|
|
27165
28918
|
return {
|
|
27166
28919
|
sessionId: trace.sessionId,
|
|
27167
28920
|
userId: trace.user.id,
|
|
28921
|
+
userDisplayName: trace.user.displayName ?? null,
|
|
27168
28922
|
gitRepo: trace.environment.gitRepo ?? null,
|
|
27169
28923
|
gitBranch: trace.environment.gitBranch ?? null,
|
|
27170
28924
|
startedAt: trace.startedAt,
|
|
@@ -27178,6 +28932,353 @@ function toSessionSummary(trace) {
|
|
|
27178
28932
|
};
|
|
27179
28933
|
}
|
|
27180
28934
|
|
|
28935
|
+
// packages/api/src/team-handler.ts
|
|
28936
|
+
function currentYearMonth() {
|
|
28937
|
+
return (/* @__PURE__ */ new Date()).toISOString().slice(0, 7);
|
|
28938
|
+
}
|
|
28939
|
+
function defaultDateRange() {
|
|
28940
|
+
const now = /* @__PURE__ */ new Date();
|
|
28941
|
+
const year = now.getFullYear();
|
|
28942
|
+
const month = String(now.getMonth() + 1).padStart(2, "0");
|
|
28943
|
+
const from = `${year}-${month}-01`;
|
|
28944
|
+
const lastDay = new Date(year, now.getMonth() + 1, 0).getDate();
|
|
28945
|
+
const to = `${year}-${month}-${String(lastDay).padStart(2, "0")}`;
|
|
28946
|
+
return { from, to };
|
|
28947
|
+
}
|
|
28948
|
+
function parseDateRange(searchParams) {
|
|
28949
|
+
const fromParam = searchParams.get("from");
|
|
28950
|
+
const toParam = searchParams.get("to");
|
|
28951
|
+
const defaults = defaultDateRange();
|
|
28952
|
+
return {
|
|
28953
|
+
from: fromParam !== null && fromParam.length === 10 ? fromParam : defaults.from,
|
|
28954
|
+
to: toParam !== null && toParam.length === 10 ? toParam : defaults.to
|
|
28955
|
+
};
|
|
28956
|
+
}
|
|
28957
|
+
function toMetricDate(startedAt) {
|
|
28958
|
+
const parsed = Date.parse(startedAt);
|
|
28959
|
+
if (Number.isNaN(parsed)) {
|
|
28960
|
+
return startedAt.slice(0, 10);
|
|
28961
|
+
}
|
|
28962
|
+
return new Date(parsed).toISOString().slice(0, 10);
|
|
28963
|
+
}
|
|
28964
|
+
function handleTeamOverview(searchParams, dependencies) {
|
|
28965
|
+
const { from, to } = parseDateRange(searchParams);
|
|
28966
|
+
const filters = { from, to };
|
|
28967
|
+
const traces = dependencies.repository.list(filters);
|
|
28968
|
+
let totalCostUsd = 0;
|
|
28969
|
+
let totalCommits = 0;
|
|
28970
|
+
let totalPullRequests = 0;
|
|
28971
|
+
let totalLinesAdded = 0;
|
|
28972
|
+
let totalLinesRemoved = 0;
|
|
28973
|
+
const memberIds = /* @__PURE__ */ new Set();
|
|
28974
|
+
for (const trace of traces) {
|
|
28975
|
+
totalCostUsd += trace.metrics.totalCostUsd;
|
|
28976
|
+
totalCommits += trace.git.commits.length;
|
|
28977
|
+
totalPullRequests += trace.git.pullRequests.length;
|
|
28978
|
+
totalLinesAdded += trace.metrics.linesAdded;
|
|
28979
|
+
totalLinesRemoved += trace.metrics.linesRemoved;
|
|
28980
|
+
if (trace.user.id !== "unknown_user") {
|
|
28981
|
+
memberIds.add(trace.user.id);
|
|
28982
|
+
}
|
|
28983
|
+
}
|
|
28984
|
+
const payload = {
|
|
28985
|
+
status: "ok",
|
|
28986
|
+
period: { from, to },
|
|
28987
|
+
totalCostUsd: Number(totalCostUsd.toFixed(6)),
|
|
28988
|
+
totalSessions: traces.length,
|
|
28989
|
+
totalCommits,
|
|
28990
|
+
totalPullRequests,
|
|
28991
|
+
totalLinesAdded,
|
|
28992
|
+
totalLinesRemoved,
|
|
28993
|
+
memberCount: memberIds.size,
|
|
28994
|
+
costPerCommit: totalCommits > 0 ? Number((totalCostUsd / totalCommits).toFixed(2)) : 0,
|
|
28995
|
+
costPerPullRequest: totalPullRequests > 0 ? Number((totalCostUsd / totalPullRequests).toFixed(2)) : 0
|
|
28996
|
+
};
|
|
28997
|
+
return { statusCode: 200, payload };
|
|
28998
|
+
}
|
|
28999
|
+
function handleTeamMembers(searchParams, dependencies) {
|
|
29000
|
+
const { from, to } = parseDateRange(searchParams);
|
|
29001
|
+
const filters = { from, to };
|
|
29002
|
+
const traces = dependencies.repository.list(filters);
|
|
29003
|
+
const memberMap = /* @__PURE__ */ new Map();
|
|
29004
|
+
for (const trace of traces) {
|
|
29005
|
+
const userId = trace.user.id;
|
|
29006
|
+
if (userId === "unknown_user") continue;
|
|
29007
|
+
const existing = memberMap.get(userId) ?? {
|
|
29008
|
+
displayName: trace.user.displayName ?? null,
|
|
29009
|
+
sessionCount: 0,
|
|
29010
|
+
totalCostUsd: 0,
|
|
29011
|
+
commitCount: 0,
|
|
29012
|
+
prCount: 0,
|
|
29013
|
+
linesAdded: 0,
|
|
29014
|
+
linesRemoved: 0,
|
|
29015
|
+
lastActiveAt: trace.startedAt
|
|
29016
|
+
};
|
|
29017
|
+
existing.sessionCount += 1;
|
|
29018
|
+
existing.totalCostUsd += trace.metrics.totalCostUsd;
|
|
29019
|
+
existing.commitCount += trace.git.commits.length;
|
|
29020
|
+
existing.prCount += trace.git.pullRequests.length;
|
|
29021
|
+
existing.linesAdded += trace.metrics.linesAdded;
|
|
29022
|
+
existing.linesRemoved += trace.metrics.linesRemoved;
|
|
29023
|
+
if (trace.user.displayName !== void 0) {
|
|
29024
|
+
existing.displayName = trace.user.displayName;
|
|
29025
|
+
}
|
|
29026
|
+
const traceEnd = trace.endedAt ?? trace.startedAt;
|
|
29027
|
+
if (traceEnd > existing.lastActiveAt) {
|
|
29028
|
+
existing.lastActiveAt = traceEnd;
|
|
29029
|
+
}
|
|
29030
|
+
memberMap.set(userId, existing);
|
|
29031
|
+
}
|
|
29032
|
+
const members = [...memberMap.entries()].map(([userId, data]) => ({
|
|
29033
|
+
userId,
|
|
29034
|
+
displayName: data.displayName,
|
|
29035
|
+
sessionCount: data.sessionCount,
|
|
29036
|
+
totalCostUsd: Number(data.totalCostUsd.toFixed(6)),
|
|
29037
|
+
commitCount: data.commitCount,
|
|
29038
|
+
prCount: data.prCount,
|
|
29039
|
+
linesAdded: data.linesAdded,
|
|
29040
|
+
linesRemoved: data.linesRemoved,
|
|
29041
|
+
costPerCommit: data.commitCount > 0 ? Number((data.totalCostUsd / data.commitCount).toFixed(2)) : 0,
|
|
29042
|
+
lastActiveAt: data.lastActiveAt
|
|
29043
|
+
})).sort((a, b) => b.totalCostUsd - a.totalCostUsd);
|
|
29044
|
+
const payload = { status: "ok", members };
|
|
29045
|
+
return { statusCode: 200, payload };
|
|
29046
|
+
}
|
|
29047
|
+
function handleTeamCostDaily(searchParams, dependencies) {
|
|
29048
|
+
const { from, to } = parseDateRange(searchParams);
|
|
29049
|
+
const filters = { from, to };
|
|
29050
|
+
const traces = dependencies.repository.list(filters);
|
|
29051
|
+
const byDate = /* @__PURE__ */ new Map();
|
|
29052
|
+
for (const trace of traces) {
|
|
29053
|
+
const date = toMetricDate(trace.startedAt);
|
|
29054
|
+
const entry = byDate.get(date) ?? {
|
|
29055
|
+
totalCostUsd: 0,
|
|
29056
|
+
sessionCount: 0,
|
|
29057
|
+
byMember: /* @__PURE__ */ new Map()
|
|
29058
|
+
};
|
|
29059
|
+
entry.totalCostUsd += trace.metrics.totalCostUsd;
|
|
29060
|
+
entry.sessionCount += 1;
|
|
29061
|
+
const userId = trace.user.id;
|
|
29062
|
+
const memberEntry = entry.byMember.get(userId) ?? { totalCostUsd: 0, sessionCount: 0 };
|
|
29063
|
+
memberEntry.totalCostUsd += trace.metrics.totalCostUsd;
|
|
29064
|
+
memberEntry.sessionCount += 1;
|
|
29065
|
+
entry.byMember.set(userId, memberEntry);
|
|
29066
|
+
byDate.set(date, entry);
|
|
29067
|
+
}
|
|
29068
|
+
const points = [...byDate.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([date, entry]) => {
|
|
29069
|
+
const byMember = [...entry.byMember.entries()].map(
|
|
29070
|
+
([userId, m]) => ({
|
|
29071
|
+
userId,
|
|
29072
|
+
totalCostUsd: Number(m.totalCostUsd.toFixed(6)),
|
|
29073
|
+
sessionCount: m.sessionCount
|
|
29074
|
+
})
|
|
29075
|
+
);
|
|
29076
|
+
return {
|
|
29077
|
+
date,
|
|
29078
|
+
totalCostUsd: Number(entry.totalCostUsd.toFixed(6)),
|
|
29079
|
+
sessionCount: entry.sessionCount,
|
|
29080
|
+
byMember
|
|
29081
|
+
};
|
|
29082
|
+
});
|
|
29083
|
+
const payload = { status: "ok", points };
|
|
29084
|
+
return { statusCode: 200, payload };
|
|
29085
|
+
}
|
|
29086
|
+
function handleGetTeamBudget(dependencies) {
|
|
29087
|
+
const budget = dependencies.teamBudgetStore?.getTeamBudget() ?? null;
|
|
29088
|
+
const yearMonth = currentYearMonth();
|
|
29089
|
+
const currentMonthSpend = dependencies.teamBudgetStore?.getMonthSpend(yearMonth) ?? 0;
|
|
29090
|
+
const percentUsed = budget !== null && budget.monthlyLimitUsd > 0 ? Number((currentMonthSpend / budget.monthlyLimitUsd * 100).toFixed(1)) : 0;
|
|
29091
|
+
const payload = {
|
|
29092
|
+
status: "ok",
|
|
29093
|
+
budget,
|
|
29094
|
+
currentMonthSpend: Number(currentMonthSpend.toFixed(6)),
|
|
29095
|
+
percentUsed
|
|
29096
|
+
};
|
|
29097
|
+
return { statusCode: 200, payload };
|
|
29098
|
+
}
|
|
29099
|
+
function handlePostTeamBudget(body, dependencies) {
|
|
29100
|
+
if (dependencies.teamBudgetStore === void 0) {
|
|
29101
|
+
return {
|
|
29102
|
+
statusCode: 501,
|
|
29103
|
+
payload: { status: "error", message: "budget storage not available" }
|
|
29104
|
+
};
|
|
29105
|
+
}
|
|
29106
|
+
if (typeof body !== "object" || body === null || Array.isArray(body)) {
|
|
29107
|
+
return {
|
|
29108
|
+
statusCode: 400,
|
|
29109
|
+
payload: { status: "error", message: "invalid request body" }
|
|
29110
|
+
};
|
|
29111
|
+
}
|
|
29112
|
+
const record = body;
|
|
29113
|
+
const monthlyLimitUsd = record["monthlyLimitUsd"];
|
|
29114
|
+
if (typeof monthlyLimitUsd !== "number" || !Number.isFinite(monthlyLimitUsd) || monthlyLimitUsd < 0) {
|
|
29115
|
+
return {
|
|
29116
|
+
statusCode: 400,
|
|
29117
|
+
payload: { status: "error", message: "monthlyLimitUsd must be a non-negative number" }
|
|
29118
|
+
};
|
|
29119
|
+
}
|
|
29120
|
+
const alertThresholdPercent = typeof record["alertThresholdPercent"] === "number" && Number.isFinite(record["alertThresholdPercent"]) && record["alertThresholdPercent"] >= 0 ? record["alertThresholdPercent"] : 80;
|
|
29121
|
+
dependencies.teamBudgetStore.upsertTeamBudget(monthlyLimitUsd, alertThresholdPercent);
|
|
29122
|
+
const payload = {
|
|
29123
|
+
status: "ok",
|
|
29124
|
+
budget: { monthlyLimitUsd, alertThresholdPercent }
|
|
29125
|
+
};
|
|
29126
|
+
return { statusCode: 200, payload };
|
|
29127
|
+
}
|
|
29128
|
+
function handleTeamAnalytics(searchParams, dependencies) {
|
|
29129
|
+
const { from, to } = parseDateRange(searchParams);
|
|
29130
|
+
const filters = { from, to };
|
|
29131
|
+
const traces = dependencies.repository.list(filters);
|
|
29132
|
+
const memberMap = /* @__PURE__ */ new Map();
|
|
29133
|
+
const teamModels = /* @__PURE__ */ new Map();
|
|
29134
|
+
const teamTools = /* @__PURE__ */ new Map();
|
|
29135
|
+
const heatmap = /* @__PURE__ */ new Map();
|
|
29136
|
+
const dailyCost = /* @__PURE__ */ new Map();
|
|
29137
|
+
let totalCost = 0;
|
|
29138
|
+
let totalCommits = 0;
|
|
29139
|
+
let totalTokens = 0;
|
|
29140
|
+
for (const trace of traces) {
|
|
29141
|
+
const userId = trace.user.id;
|
|
29142
|
+
if (userId === "unknown_user") continue;
|
|
29143
|
+
const startDate = new Date(trace.startedAt);
|
|
29144
|
+
const hour = startDate.getUTCHours();
|
|
29145
|
+
const dayOfWeek = startDate.getUTCDay();
|
|
29146
|
+
const dateStr = toMetricDate(trace.startedAt);
|
|
29147
|
+
const member = memberMap.get(userId) ?? {
|
|
29148
|
+
displayName: trace.user.displayName ?? null,
|
|
29149
|
+
totalCostUsd: 0,
|
|
29150
|
+
sessionCount: 0,
|
|
29151
|
+
commitCount: 0,
|
|
29152
|
+
prCount: 0,
|
|
29153
|
+
linesAdded: 0,
|
|
29154
|
+
linesRemoved: 0,
|
|
29155
|
+
totalInputTokens: 0,
|
|
29156
|
+
totalOutputTokens: 0,
|
|
29157
|
+
models: /* @__PURE__ */ new Map(),
|
|
29158
|
+
tools: /* @__PURE__ */ new Map(),
|
|
29159
|
+
repos: /* @__PURE__ */ new Map(),
|
|
29160
|
+
hourlyActivity: new Array(24).fill(0),
|
|
29161
|
+
dailyActivity: new Array(7).fill(0)
|
|
29162
|
+
};
|
|
29163
|
+
member.totalCostUsd += trace.metrics.totalCostUsd;
|
|
29164
|
+
member.sessionCount += 1;
|
|
29165
|
+
member.commitCount += trace.git.commits.length;
|
|
29166
|
+
member.prCount += trace.git.pullRequests.length;
|
|
29167
|
+
member.linesAdded += trace.metrics.linesAdded;
|
|
29168
|
+
member.linesRemoved += trace.metrics.linesRemoved;
|
|
29169
|
+
member.totalInputTokens += trace.metrics.totalInputTokens;
|
|
29170
|
+
member.totalOutputTokens += trace.metrics.totalOutputTokens;
|
|
29171
|
+
member.hourlyActivity[hour] = (member.hourlyActivity[hour] ?? 0) + 1;
|
|
29172
|
+
member.dailyActivity[dayOfWeek] = (member.dailyActivity[dayOfWeek] ?? 0) + 1;
|
|
29173
|
+
if (trace.user.displayName !== void 0) {
|
|
29174
|
+
member.displayName = trace.user.displayName;
|
|
29175
|
+
}
|
|
29176
|
+
const costPerModel = trace.metrics.modelsUsed.length > 0 ? trace.metrics.totalCostUsd / trace.metrics.modelsUsed.length : 0;
|
|
29177
|
+
for (const model of trace.metrics.modelsUsed) {
|
|
29178
|
+
const existing = member.models.get(model) ?? { count: 0, costUsd: 0 };
|
|
29179
|
+
existing.count += 1;
|
|
29180
|
+
existing.costUsd += costPerModel;
|
|
29181
|
+
member.models.set(model, existing);
|
|
29182
|
+
const teamEntry = teamModels.get(model) ?? { count: 0, costUsd: 0 };
|
|
29183
|
+
teamEntry.count += 1;
|
|
29184
|
+
teamEntry.costUsd += costPerModel;
|
|
29185
|
+
teamModels.set(model, teamEntry);
|
|
29186
|
+
}
|
|
29187
|
+
for (const tool of trace.metrics.toolsUsed) {
|
|
29188
|
+
member.tools.set(tool, (member.tools.get(tool) ?? 0) + 1);
|
|
29189
|
+
teamTools.set(tool, (teamTools.get(tool) ?? 0) + 1);
|
|
29190
|
+
}
|
|
29191
|
+
for (const event of trace.timeline) {
|
|
29192
|
+
if (event.type === "tool_call" || event.type === "tool_result") {
|
|
29193
|
+
const details = event.details;
|
|
29194
|
+
const toolName = details !== void 0 ? details["toolName"] : void 0;
|
|
29195
|
+
if (toolName !== void 0 && toolName.length > 0) {
|
|
29196
|
+
member.tools.set(toolName, (member.tools.get(toolName) ?? 0) + 1);
|
|
29197
|
+
teamTools.set(toolName, (teamTools.get(toolName) ?? 0) + 1);
|
|
29198
|
+
}
|
|
29199
|
+
}
|
|
29200
|
+
}
|
|
29201
|
+
const repo = trace.environment.gitRepo;
|
|
29202
|
+
if (repo !== void 0 && repo.length > 0) {
|
|
29203
|
+
const repoEntry = member.repos.get(repo) ?? { sessions: 0, commits: 0 };
|
|
29204
|
+
repoEntry.sessions += 1;
|
|
29205
|
+
repoEntry.commits += trace.git.commits.length;
|
|
29206
|
+
member.repos.set(repo, repoEntry);
|
|
29207
|
+
}
|
|
29208
|
+
memberMap.set(userId, member);
|
|
29209
|
+
const heatKey = `${String(hour)}-${String(dayOfWeek)}`;
|
|
29210
|
+
heatmap.set(heatKey, (heatmap.get(heatKey) ?? 0) + 1);
|
|
29211
|
+
const dayEntry = dailyCost.get(dateStr) ?? { costUsd: 0, sessions: 0 };
|
|
29212
|
+
dayEntry.costUsd += trace.metrics.totalCostUsd;
|
|
29213
|
+
dayEntry.sessions += 1;
|
|
29214
|
+
dailyCost.set(dateStr, dayEntry);
|
|
29215
|
+
totalCost += trace.metrics.totalCostUsd;
|
|
29216
|
+
totalCommits += trace.git.commits.length;
|
|
29217
|
+
totalTokens += trace.metrics.totalInputTokens + trace.metrics.totalOutputTokens;
|
|
29218
|
+
}
|
|
29219
|
+
const memberAnalytics = [...memberMap.entries()].map(([userId, m]) => {
|
|
29220
|
+
const models = [...m.models.entries()].map(([model, data]) => ({ model, sessionCount: data.count, totalCostUsd: Number(data.costUsd.toFixed(6)) })).sort((a, b) => b.totalCostUsd - a.totalCostUsd);
|
|
29221
|
+
const tools = [...m.tools.entries()].map(([tool, callCount]) => ({ tool, callCount })).sort((a, b) => b.callCount - a.callCount);
|
|
29222
|
+
const repos = [...m.repos.entries()].map(([repo, data]) => ({ repo, sessionCount: data.sessions, commitCount: data.commits })).sort((a, b) => b.sessionCount - a.sessionCount);
|
|
29223
|
+
return {
|
|
29224
|
+
userId,
|
|
29225
|
+
displayName: m.displayName,
|
|
29226
|
+
totalCostUsd: Number(m.totalCostUsd.toFixed(6)),
|
|
29227
|
+
sessionCount: m.sessionCount,
|
|
29228
|
+
commitCount: m.commitCount,
|
|
29229
|
+
prCount: m.prCount,
|
|
29230
|
+
linesAdded: m.linesAdded,
|
|
29231
|
+
linesRemoved: m.linesRemoved,
|
|
29232
|
+
avgSessionCostUsd: m.sessionCount > 0 ? Number((m.totalCostUsd / m.sessionCount).toFixed(4)) : 0,
|
|
29233
|
+
costPerCommit: m.commitCount > 0 ? Number((m.totalCostUsd / m.commitCount).toFixed(2)) : 0,
|
|
29234
|
+
totalInputTokens: m.totalInputTokens,
|
|
29235
|
+
totalOutputTokens: m.totalOutputTokens,
|
|
29236
|
+
models,
|
|
29237
|
+
tools,
|
|
29238
|
+
repos,
|
|
29239
|
+
hourlyActivity: m.hourlyActivity,
|
|
29240
|
+
dailyActivity: m.dailyActivity
|
|
29241
|
+
};
|
|
29242
|
+
}).sort((a, b) => b.totalCostUsd - a.totalCostUsd);
|
|
29243
|
+
const topModels = [...teamModels.entries()].map(([model, data]) => ({ model, sessionCount: data.count, totalCostUsd: Number(data.costUsd.toFixed(6)) })).sort((a, b) => b.totalCostUsd - a.totalCostUsd).slice(0, 10);
|
|
29244
|
+
const topTools = [...teamTools.entries()].map(([tool, callCount]) => ({ tool, callCount })).sort((a, b) => b.callCount - a.callCount).slice(0, 15);
|
|
29245
|
+
const hourlyHeatmap = [];
|
|
29246
|
+
for (let day = 0; day < 7; day++) {
|
|
29247
|
+
for (let hour = 0; hour < 24; hour++) {
|
|
29248
|
+
const count = heatmap.get(`${String(hour)}-${String(day)}`) ?? 0;
|
|
29249
|
+
hourlyHeatmap.push({ hour, day, count });
|
|
29250
|
+
}
|
|
29251
|
+
}
|
|
29252
|
+
let cumulative = 0;
|
|
29253
|
+
const costTrend = [...dailyCost.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([date, data]) => {
|
|
29254
|
+
cumulative += data.costUsd;
|
|
29255
|
+
return {
|
|
29256
|
+
date,
|
|
29257
|
+
costUsd: Number(data.costUsd.toFixed(6)),
|
|
29258
|
+
sessionCount: data.sessions,
|
|
29259
|
+
cumulativeCostUsd: Number(cumulative.toFixed(6))
|
|
29260
|
+
};
|
|
29261
|
+
});
|
|
29262
|
+
const totalSessions = traces.filter((t) => t.user.id !== "unknown_user").length;
|
|
29263
|
+
const commitsPerDollar = totalCost > 0 ? totalCommits / totalCost : 0;
|
|
29264
|
+
const costEfficiencyScore = Math.min(100, Math.round(commitsPerDollar * 20));
|
|
29265
|
+
const payload = {
|
|
29266
|
+
status: "ok",
|
|
29267
|
+
period: { from, to },
|
|
29268
|
+
memberAnalytics,
|
|
29269
|
+
topModels,
|
|
29270
|
+
topTools,
|
|
29271
|
+
hourlyHeatmap,
|
|
29272
|
+
costTrend,
|
|
29273
|
+
avgCostPerSession: totalSessions > 0 ? Number((totalCost / totalSessions).toFixed(4)) : 0,
|
|
29274
|
+
avgCommitsPerSession: totalSessions > 0 ? Number((totalCommits / totalSessions).toFixed(2)) : 0,
|
|
29275
|
+
avgCostPerCommit: totalCommits > 0 ? Number((totalCost / totalCommits).toFixed(2)) : 0,
|
|
29276
|
+
totalTokensUsed: totalTokens,
|
|
29277
|
+
costEfficiencyScore
|
|
29278
|
+
};
|
|
29279
|
+
return { statusCode: 200, payload };
|
|
29280
|
+
}
|
|
29281
|
+
|
|
27181
29282
|
// packages/api/src/handler.ts
|
|
27182
29283
|
function buildError(message) {
|
|
27183
29284
|
return {
|
|
@@ -27195,15 +29296,19 @@ function buildHealth(startedAtMs) {
|
|
|
27195
29296
|
function parseFilters(searchParams) {
|
|
27196
29297
|
const userId = searchParams.get("userId");
|
|
27197
29298
|
const repo = searchParams.get("repo");
|
|
29299
|
+
const from = searchParams.get("from");
|
|
29300
|
+
const to = searchParams.get("to");
|
|
27198
29301
|
return {
|
|
27199
29302
|
...userId !== null ? { userId } : {},
|
|
27200
|
-
...repo !== null ? { repo } : {}
|
|
29303
|
+
...repo !== null ? { repo } : {},
|
|
29304
|
+
...from !== null && from.length === 10 ? { from } : {},
|
|
29305
|
+
...to !== null && to.length === 10 ? { to } : {}
|
|
27201
29306
|
};
|
|
27202
29307
|
}
|
|
27203
29308
|
function parseSessionPath(pathname) {
|
|
27204
29309
|
return pathname.split("/").filter((segment) => segment.length > 0);
|
|
27205
29310
|
}
|
|
27206
|
-
function
|
|
29311
|
+
function toMetricDate2(startedAt) {
|
|
27207
29312
|
const parsed = Date.parse(startedAt);
|
|
27208
29313
|
if (Number.isNaN(parsed)) {
|
|
27209
29314
|
return startedAt.slice(0, 10);
|
|
@@ -27214,7 +29319,7 @@ function buildDailyCostResponseFromTraces(dependencies, filters) {
|
|
|
27214
29319
|
const traces = dependencies.repository.list(filters);
|
|
27215
29320
|
const byDate = /* @__PURE__ */ new Map();
|
|
27216
29321
|
traces.forEach((trace) => {
|
|
27217
|
-
const date =
|
|
29322
|
+
const date = toMetricDate2(trace.startedAt);
|
|
27218
29323
|
const current = byDate.get(date) ?? {
|
|
27219
29324
|
totalCostUsd: 0,
|
|
27220
29325
|
sessionCount: 0,
|
|
@@ -27278,6 +29383,33 @@ async function handleApiRequest(request, dependencies) {
|
|
|
27278
29383
|
payload: await buildDailyCostResponse(dependencies, filters)
|
|
27279
29384
|
};
|
|
27280
29385
|
}
|
|
29386
|
+
if (request.method === "GET" && pathname === "/v1/team/overview") {
|
|
29387
|
+
return handleTeamOverview(parsedUrl.searchParams, dependencies);
|
|
29388
|
+
}
|
|
29389
|
+
if (request.method === "GET" && pathname === "/v1/team/members") {
|
|
29390
|
+
return handleTeamMembers(parsedUrl.searchParams, dependencies);
|
|
29391
|
+
}
|
|
29392
|
+
if (request.method === "GET" && pathname === "/v1/team/cost/daily") {
|
|
29393
|
+
return handleTeamCostDaily(parsedUrl.searchParams, dependencies);
|
|
29394
|
+
}
|
|
29395
|
+
if (request.method === "GET" && pathname === "/v1/team/analytics") {
|
|
29396
|
+
return handleTeamAnalytics(parsedUrl.searchParams, dependencies);
|
|
29397
|
+
}
|
|
29398
|
+
if (request.method === "GET" && pathname === "/v1/team/budget") {
|
|
29399
|
+
return handleGetTeamBudget(dependencies);
|
|
29400
|
+
}
|
|
29401
|
+
if (request.method === "POST" && pathname === "/v1/team/budget") {
|
|
29402
|
+
return handlePostTeamBudget(request.body, dependencies);
|
|
29403
|
+
}
|
|
29404
|
+
if (request.method === "GET" && pathname === "/v1/team/insights/context") {
|
|
29405
|
+
return handleGetTeamInsightsContext(dependencies);
|
|
29406
|
+
}
|
|
29407
|
+
if (request.method === "POST" && pathname === "/v1/team/insights/context") {
|
|
29408
|
+
return handlePostTeamInsightsContext(request.body, dependencies);
|
|
29409
|
+
}
|
|
29410
|
+
if (request.method === "POST" && pathname === "/v1/team/insights/generate") {
|
|
29411
|
+
return handlePostTeamInsight(parsedUrl.searchParams, dependencies);
|
|
29412
|
+
}
|
|
27281
29413
|
if (request.method === "GET" && pathname === "/v1/settings/insights") {
|
|
27282
29414
|
return handleGetInsightsSettings(dependencies);
|
|
27283
29415
|
}
|
|
@@ -27340,6 +29472,16 @@ function normalizeMethod(method) {
|
|
|
27340
29472
|
}
|
|
27341
29473
|
return void 0;
|
|
27342
29474
|
}
|
|
29475
|
+
function checkApiAuth(authorizationHeader) {
|
|
29476
|
+
const teamAuthToken = process.env["TEAM_AUTH_TOKEN"];
|
|
29477
|
+
if (teamAuthToken === void 0 || teamAuthToken.length === 0) {
|
|
29478
|
+
return true;
|
|
29479
|
+
}
|
|
29480
|
+
if (authorizationHeader === void 0 || authorizationHeader.length === 0) {
|
|
29481
|
+
return false;
|
|
29482
|
+
}
|
|
29483
|
+
return authorizationHeader === `Bearer ${teamAuthToken}`;
|
|
29484
|
+
}
|
|
27343
29485
|
function sendJson2(res, statusCode, payload) {
|
|
27344
29486
|
const body = JSON.stringify(payload);
|
|
27345
29487
|
res.statusCode = statusCode;
|
|
@@ -27433,6 +29575,18 @@ function createApiHttpHandler(dependencies) {
|
|
|
27433
29575
|
return (req, res) => {
|
|
27434
29576
|
const method = req.method ?? "GET";
|
|
27435
29577
|
const url = req.url ?? "/";
|
|
29578
|
+
const authHeader = typeof req.headers["authorization"] === "string" ? req.headers["authorization"] : void 0;
|
|
29579
|
+
if (method === "GET" && parsePathname2(url) === "/v1/auth/check") {
|
|
29580
|
+
const teamAuthToken = process.env["TEAM_AUTH_TOKEN"];
|
|
29581
|
+
const authRequired = teamAuthToken !== void 0 && teamAuthToken.length > 0;
|
|
29582
|
+
const authValid = !authRequired || checkApiAuth(authHeader);
|
|
29583
|
+
sendJson2(res, 200, { status: "ok", authRequired, authValid });
|
|
29584
|
+
return;
|
|
29585
|
+
}
|
|
29586
|
+
if (!checkApiAuth(authHeader)) {
|
|
29587
|
+
sendJson2(res, 401, { status: "error", message: "authorization required" });
|
|
29588
|
+
return;
|
|
29589
|
+
}
|
|
27436
29590
|
if (isSseSessionsRoute(method, url)) {
|
|
27437
29591
|
startSessionsSseStream(req, res, dependencies);
|
|
27438
29592
|
return;
|
|
@@ -27481,6 +29635,18 @@ var InMemorySessionRepository = class {
|
|
|
27481
29635
|
if (filters.repo !== void 0 && trace.environment.gitRepo !== filters.repo) {
|
|
27482
29636
|
return false;
|
|
27483
29637
|
}
|
|
29638
|
+
if (filters.from !== void 0) {
|
|
29639
|
+
const traceDate = trace.startedAt.slice(0, 10);
|
|
29640
|
+
if (traceDate < filters.from) {
|
|
29641
|
+
return false;
|
|
29642
|
+
}
|
|
29643
|
+
}
|
|
29644
|
+
if (filters.to !== void 0) {
|
|
29645
|
+
const traceDate = trace.startedAt.slice(0, 10);
|
|
29646
|
+
if (traceDate > filters.to) {
|
|
29647
|
+
return false;
|
|
29648
|
+
}
|
|
29649
|
+
}
|
|
27484
29650
|
return true;
|
|
27485
29651
|
});
|
|
27486
29652
|
}
|
|
@@ -27734,6 +29900,26 @@ function parsePathname3(url) {
|
|
|
27734
29900
|
return url;
|
|
27735
29901
|
}
|
|
27736
29902
|
}
|
|
29903
|
+
function checkTeamAuth(authorizationHeader) {
|
|
29904
|
+
const teamAuthToken = process.env["TEAM_AUTH_TOKEN"];
|
|
29905
|
+
if (teamAuthToken === void 0 || teamAuthToken.length === 0) {
|
|
29906
|
+
return void 0;
|
|
29907
|
+
}
|
|
29908
|
+
if (authorizationHeader === void 0 || authorizationHeader.length === 0) {
|
|
29909
|
+
return {
|
|
29910
|
+
statusCode: 401,
|
|
29911
|
+
payload: { status: "error", message: "authorization required" }
|
|
29912
|
+
};
|
|
29913
|
+
}
|
|
29914
|
+
const expected = `Bearer ${teamAuthToken}`;
|
|
29915
|
+
if (authorizationHeader !== expected) {
|
|
29916
|
+
return {
|
|
29917
|
+
statusCode: 403,
|
|
29918
|
+
payload: { status: "error", message: "invalid authorization token" }
|
|
29919
|
+
};
|
|
29920
|
+
}
|
|
29921
|
+
return void 0;
|
|
29922
|
+
}
|
|
27737
29923
|
function handleCollectorRawHttpRequest(request, dependencies) {
|
|
27738
29924
|
const method = normalizeMethod2(request.method);
|
|
27739
29925
|
if (method === void 0) {
|
|
@@ -27745,6 +29931,12 @@ function handleCollectorRawHttpRequest(request, dependencies) {
|
|
|
27745
29931
|
}
|
|
27746
29932
|
};
|
|
27747
29933
|
}
|
|
29934
|
+
if (method === "POST") {
|
|
29935
|
+
const authError = checkTeamAuth(request.authorizationHeader);
|
|
29936
|
+
if (authError !== void 0) {
|
|
29937
|
+
return authError;
|
|
29938
|
+
}
|
|
29939
|
+
}
|
|
27748
29940
|
const pathname = parsePathname3(request.url);
|
|
27749
29941
|
if (method === "POST" && pathname === "/v1/hooks") {
|
|
27750
29942
|
const rawBody = request.rawBody ?? "";
|
|
@@ -27805,11 +29997,13 @@ function createCollectorHttpHandler(dependencies) {
|
|
|
27805
29997
|
const method = req.method ?? "GET";
|
|
27806
29998
|
const url = req.url ?? "/";
|
|
27807
29999
|
const rawBody = method === "POST" ? await readRequestBody2(req) : void 0;
|
|
30000
|
+
const authorizationHeader = typeof req.headers["authorization"] === "string" ? req.headers["authorization"] : void 0;
|
|
27808
30001
|
const response = handleCollectorRawHttpRequest(
|
|
27809
30002
|
{
|
|
27810
30003
|
method,
|
|
27811
30004
|
url,
|
|
27812
|
-
...rawBody !== void 0 ? { rawBody } : {}
|
|
30005
|
+
...rawBody !== void 0 ? { rawBody } : {},
|
|
30006
|
+
...authorizationHeader !== void 0 ? { authorizationHeader } : {}
|
|
27813
30007
|
},
|
|
27814
30008
|
dependencies
|
|
27815
30009
|
);
|
|
@@ -29298,12 +31492,16 @@ function toBaseTrace(envelope) {
|
|
|
29298
31492
|
const gitRepo = readString4(payload, ["git_repo", "gitRepo"]);
|
|
29299
31493
|
const gitBranch = readString4(payload, ["git_branch", "gitBranch"]);
|
|
29300
31494
|
const projectPath = readString4(payload, ["project_path", "projectPath"]);
|
|
29301
|
-
const
|
|
31495
|
+
const userEmail = readString4(payload, ["user_email", "userEmail"]);
|
|
31496
|
+
const userName = readString4(payload, ["user_name", "userName"]);
|
|
31497
|
+
const userId = userEmail ?? readString4(payload, ["user_id", "userId"]) ?? "unknown_user";
|
|
29302
31498
|
return {
|
|
29303
31499
|
sessionId: envelope.sessionId,
|
|
29304
31500
|
agentType: "claude_code",
|
|
29305
31501
|
user: {
|
|
29306
|
-
id: userId
|
|
31502
|
+
id: userId,
|
|
31503
|
+
...userEmail !== void 0 ? { email: userEmail } : {},
|
|
31504
|
+
...userName !== void 0 ? { displayName: userName } : {}
|
|
29307
31505
|
},
|
|
29308
31506
|
environment: {
|
|
29309
31507
|
...projectPath !== void 0 ? { projectPath } : {},
|
|
@@ -29360,6 +31558,13 @@ function toUpdatedTrace(existing, envelope) {
|
|
|
29360
31558
|
const mergedTimeline = [...existing.timeline, timelineEvent];
|
|
29361
31559
|
const endedAt = shouldMarkEnded(envelope.eventType) ? envelope.eventTimestamp : existing.endedAt;
|
|
29362
31560
|
const latestTime = endedAt ?? envelope.eventTimestamp;
|
|
31561
|
+
const newEmail = readString4(payload, ["user_email", "userEmail"]);
|
|
31562
|
+
const newName = readString4(payload, ["user_name", "userName"]);
|
|
31563
|
+
const updatedUser = existing.user.id === "unknown_user" && newEmail !== void 0 ? { id: newEmail, email: newEmail, ...newName !== void 0 ? { displayName: newName } : {} } : {
|
|
31564
|
+
...existing.user,
|
|
31565
|
+
...existing.user.email === void 0 && newEmail !== void 0 ? { email: newEmail } : {},
|
|
31566
|
+
...existing.user.displayName === void 0 && newName !== void 0 ? { displayName: newName } : {}
|
|
31567
|
+
};
|
|
29363
31568
|
const cost = computeEventCost(payload) ?? 0;
|
|
29364
31569
|
const inputTokens = readNumber3(payload, ["input_tokens", "inputTokens"]) ?? 0;
|
|
29365
31570
|
const outputTokens = readNumber3(payload, ["output_tokens", "outputTokens"]) ?? 0;
|
|
@@ -29428,6 +31633,7 @@ function toUpdatedTrace(existing, envelope) {
|
|
|
29428
31633
|
}
|
|
29429
31634
|
return {
|
|
29430
31635
|
...existing,
|
|
31636
|
+
user: updatedUser,
|
|
29431
31637
|
...endedAt !== void 0 ? { endedAt } : {},
|
|
29432
31638
|
activeDurationMs: updateDurationMs(existing.startedAt, latestTime),
|
|
29433
31639
|
timeline: mergedTimeline,
|
|
@@ -29513,7 +31719,8 @@ function resolveRuntimeOptions(input) {
|
|
|
29513
31719
|
startedAtMs: input,
|
|
29514
31720
|
persistence: new InMemoryRuntimePersistence(),
|
|
29515
31721
|
dailyCostReader: void 0,
|
|
29516
|
-
insightsConfigAccessor: void 0
|
|
31722
|
+
insightsConfigAccessor: void 0,
|
|
31723
|
+
teamBudgetStore: void 0
|
|
29517
31724
|
};
|
|
29518
31725
|
}
|
|
29519
31726
|
const startedAtMs = input?.startedAtMs ?? Date.now();
|
|
@@ -29522,7 +31729,8 @@ function resolveRuntimeOptions(input) {
|
|
|
29522
31729
|
startedAtMs,
|
|
29523
31730
|
persistence,
|
|
29524
31731
|
dailyCostReader: input?.dailyCostReader,
|
|
29525
|
-
insightsConfigAccessor: input?.insightsConfigAccessor
|
|
31732
|
+
insightsConfigAccessor: input?.insightsConfigAccessor,
|
|
31733
|
+
teamBudgetStore: input?.teamBudgetStore
|
|
29526
31734
|
};
|
|
29527
31735
|
}
|
|
29528
31736
|
function createInMemoryRuntime(input) {
|
|
@@ -29541,7 +31749,8 @@ function createInMemoryRuntime(input) {
|
|
|
29541
31749
|
startedAtMs: options.startedAtMs,
|
|
29542
31750
|
repository: sessionRepository,
|
|
29543
31751
|
...options.dailyCostReader !== void 0 ? { dailyCostReader: options.dailyCostReader } : {},
|
|
29544
|
-
...options.insightsConfigAccessor !== void 0 ? { insightsConfigAccessor: options.insightsConfigAccessor } : {}
|
|
31752
|
+
...options.insightsConfigAccessor !== void 0 ? { insightsConfigAccessor: options.insightsConfigAccessor } : {},
|
|
31753
|
+
...options.teamBudgetStore !== void 0 ? { teamBudgetStore: options.teamBudgetStore } : {}
|
|
29545
31754
|
};
|
|
29546
31755
|
return {
|
|
29547
31756
|
sessionRepository,
|
|
@@ -29551,6 +31760,7 @@ function createInMemoryRuntime(input) {
|
|
|
29551
31760
|
persistence,
|
|
29552
31761
|
...options.dailyCostReader !== void 0 ? { dailyCostReader: options.dailyCostReader } : {},
|
|
29553
31762
|
...options.insightsConfigAccessor !== void 0 ? { insightsConfigAccessor: options.insightsConfigAccessor } : {},
|
|
31763
|
+
...options.teamBudgetStore !== void 0 ? { teamBudgetStore: options.teamBudgetStore } : {},
|
|
29554
31764
|
handleCollectorRaw: collectorService.handleRaw,
|
|
29555
31765
|
handleApiRaw: (request) => handleApiRawHttpRequest(request, apiDependencies)
|
|
29556
31766
|
};
|
|
@@ -29572,7 +31782,8 @@ async function startInMemoryRuntimeServers(runtime, options = {}) {
|
|
|
29572
31782
|
startedAtMs: runtime.collectorDependencies.startedAtMs,
|
|
29573
31783
|
repository: runtime.sessionRepository,
|
|
29574
31784
|
...runtime.dailyCostReader !== void 0 ? { dailyCostReader: runtime.dailyCostReader } : {},
|
|
29575
|
-
...runtime.insightsConfigAccessor !== void 0 ? { insightsConfigAccessor: runtime.insightsConfigAccessor } : {}
|
|
31785
|
+
...runtime.insightsConfigAccessor !== void 0 ? { insightsConfigAccessor: runtime.insightsConfigAccessor } : {},
|
|
31786
|
+
...runtime.teamBudgetStore !== void 0 ? { teamBudgetStore: runtime.teamBudgetStore } : {}
|
|
29576
31787
|
})
|
|
29577
31788
|
) : void 0;
|
|
29578
31789
|
let otelReceiver;
|
|
@@ -29744,6 +31955,7 @@ function hydrateFromSqlite(runtime, sqlite, limit, eventLimit) {
|
|
|
29744
31955
|
var VALID_INSIGHTS_PROVIDERS = ["anthropic", "openai", "gemini", "openrouter"];
|
|
29745
31956
|
function createSqliteInsightsConfigAccessor(sqlite) {
|
|
29746
31957
|
let cached;
|
|
31958
|
+
let cachedContext;
|
|
29747
31959
|
const raw = sqlite.getSetting("insights_config");
|
|
29748
31960
|
if (raw !== void 0) {
|
|
29749
31961
|
try {
|
|
@@ -29761,6 +31973,23 @@ function createSqliteInsightsConfigAccessor(sqlite) {
|
|
|
29761
31973
|
} catch {
|
|
29762
31974
|
}
|
|
29763
31975
|
}
|
|
31976
|
+
const rawContext = sqlite.getSetting("team_insights_context");
|
|
31977
|
+
if (rawContext !== void 0) {
|
|
31978
|
+
try {
|
|
31979
|
+
const parsed = JSON.parse(rawContext);
|
|
31980
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
31981
|
+
const obj = parsed;
|
|
31982
|
+
if (typeof obj["companyContext"] === "string" || typeof obj["analysisGuidelines"] === "string") {
|
|
31983
|
+
cachedContext = {
|
|
31984
|
+
companyContext: typeof obj["companyContext"] === "string" ? obj["companyContext"] : "",
|
|
31985
|
+
analysisGuidelines: typeof obj["analysisGuidelines"] === "string" ? obj["analysisGuidelines"] : "",
|
|
31986
|
+
updatedAt: typeof obj["updatedAt"] === "string" ? obj["updatedAt"] : (/* @__PURE__ */ new Date()).toISOString()
|
|
31987
|
+
};
|
|
31988
|
+
}
|
|
31989
|
+
}
|
|
31990
|
+
} catch {
|
|
31991
|
+
}
|
|
31992
|
+
}
|
|
29764
31993
|
return {
|
|
29765
31994
|
getConfig() {
|
|
29766
31995
|
return cached;
|
|
@@ -29768,6 +31997,26 @@ function createSqliteInsightsConfigAccessor(sqlite) {
|
|
|
29768
31997
|
setConfig(config) {
|
|
29769
31998
|
cached = config;
|
|
29770
31999
|
sqlite.upsertSetting("insights_config", JSON.stringify(config));
|
|
32000
|
+
},
|
|
32001
|
+
getTeamInsightsContext() {
|
|
32002
|
+
return cachedContext;
|
|
32003
|
+
},
|
|
32004
|
+
setTeamInsightsContext(context) {
|
|
32005
|
+
cachedContext = context;
|
|
32006
|
+
sqlite.upsertSetting("team_insights_context", JSON.stringify(context));
|
|
32007
|
+
}
|
|
32008
|
+
};
|
|
32009
|
+
}
|
|
32010
|
+
function createSqliteTeamBudgetStore(sqlite) {
|
|
32011
|
+
return {
|
|
32012
|
+
getTeamBudget() {
|
|
32013
|
+
return sqlite.getTeamBudget() ?? void 0;
|
|
32014
|
+
},
|
|
32015
|
+
upsertTeamBudget(limitUsd, alertPercent) {
|
|
32016
|
+
sqlite.upsertTeamBudget(limitUsd, alertPercent);
|
|
32017
|
+
},
|
|
32018
|
+
getMonthSpend(yearMonth) {
|
|
32019
|
+
return sqlite.getMonthSpend(yearMonth);
|
|
29771
32020
|
}
|
|
29772
32021
|
};
|
|
29773
32022
|
}
|
|
@@ -29776,11 +32025,13 @@ function createSqliteBackedRuntime(options) {
|
|
|
29776
32025
|
const persistence = new SqlitePersistence(sqlite);
|
|
29777
32026
|
const dailyCostReader = new SqliteDailyCostReader(sqlite);
|
|
29778
32027
|
const insightsConfigAccessor = options.insightsConfigAccessor ?? createSqliteInsightsConfigAccessor(sqlite);
|
|
32028
|
+
const teamBudgetStore = createSqliteTeamBudgetStore(sqlite);
|
|
29779
32029
|
const runtime = createInMemoryRuntime({
|
|
29780
32030
|
...options.startedAtMs !== void 0 ? { startedAtMs: options.startedAtMs } : {},
|
|
29781
32031
|
persistence,
|
|
29782
32032
|
dailyCostReader,
|
|
29783
|
-
insightsConfigAccessor
|
|
32033
|
+
insightsConfigAccessor,
|
|
32034
|
+
teamBudgetStore
|
|
29784
32035
|
});
|
|
29785
32036
|
const hydratedCount = hydrateFromSqlite(runtime, sqlite, options.bootstrapLimit, options.eventLimit);
|
|
29786
32037
|
const syncIntervalMs = options.syncIntervalMs ?? 5e3;
|
|
@@ -29826,6 +32077,9 @@ function parseArgs(argv) {
|
|
|
29826
32077
|
let privacyTier;
|
|
29827
32078
|
let installHooks;
|
|
29828
32079
|
let forward = false;
|
|
32080
|
+
let teamUrl;
|
|
32081
|
+
let teamPrivacyTier;
|
|
32082
|
+
let teamToken;
|
|
29829
32083
|
for (let i = 3; i < argv.length; i += 1) {
|
|
29830
32084
|
const token = argv[i];
|
|
29831
32085
|
if (token === "--forward") {
|
|
@@ -29862,6 +32116,28 @@ function parseArgs(argv) {
|
|
|
29862
32116
|
i += 1;
|
|
29863
32117
|
continue;
|
|
29864
32118
|
}
|
|
32119
|
+
if (token === "--team-url") {
|
|
32120
|
+
const value = argv[i + 1];
|
|
32121
|
+
if (typeof value === "string" && value.length > 0) {
|
|
32122
|
+
teamUrl = value;
|
|
32123
|
+
}
|
|
32124
|
+
i += 1;
|
|
32125
|
+
continue;
|
|
32126
|
+
}
|
|
32127
|
+
if (token === "--team-privacy-tier") {
|
|
32128
|
+
const value = argv[i + 1];
|
|
32129
|
+
teamPrivacyTier = parsePrivacyTier(value);
|
|
32130
|
+
i += 1;
|
|
32131
|
+
continue;
|
|
32132
|
+
}
|
|
32133
|
+
if (token === "--team-token") {
|
|
32134
|
+
const value = argv[i + 1];
|
|
32135
|
+
if (typeof value === "string" && value.length > 0) {
|
|
32136
|
+
teamToken = value;
|
|
32137
|
+
}
|
|
32138
|
+
i += 1;
|
|
32139
|
+
continue;
|
|
32140
|
+
}
|
|
29865
32141
|
}
|
|
29866
32142
|
return {
|
|
29867
32143
|
command,
|
|
@@ -29869,10 +32145,16 @@ function parseArgs(argv) {
|
|
|
29869
32145
|
...collectorUrl !== void 0 ? { collectorUrl } : {},
|
|
29870
32146
|
...privacyTier !== void 0 ? { privacyTier } : {},
|
|
29871
32147
|
...installHooks !== void 0 ? { installHooks } : {},
|
|
29872
|
-
...forward ? { forward: true } : {}
|
|
32148
|
+
...forward ? { forward: true } : {},
|
|
32149
|
+
...teamUrl !== void 0 ? { teamUrl } : {},
|
|
32150
|
+
...teamPrivacyTier !== void 0 ? { teamPrivacyTier } : {},
|
|
32151
|
+
...teamToken !== void 0 ? { teamToken } : {}
|
|
29873
32152
|
};
|
|
29874
32153
|
}
|
|
29875
32154
|
|
|
32155
|
+
// packages/cli/src/init.ts
|
|
32156
|
+
var import_node_child_process = require("node:child_process");
|
|
32157
|
+
|
|
29876
32158
|
// packages/cli/src/claude-hooks.ts
|
|
29877
32159
|
var HOOK_EVENTS = [
|
|
29878
32160
|
"SessionStart",
|
|
@@ -29924,12 +32206,22 @@ function parseConfig(raw) {
|
|
|
29924
32206
|
if (typeof parsed["updatedAt"] !== "string" || parsed["updatedAt"].length === 0) {
|
|
29925
32207
|
return void 0;
|
|
29926
32208
|
}
|
|
32209
|
+
const userEmail = typeof parsed["userEmail"] === "string" && parsed["userEmail"].length > 0 ? parsed["userEmail"] : void 0;
|
|
32210
|
+
const userName = typeof parsed["userName"] === "string" && parsed["userName"].length > 0 ? parsed["userName"] : void 0;
|
|
32211
|
+
const teamCollectorUrl = typeof parsed["teamCollectorUrl"] === "string" && parsed["teamCollectorUrl"].length > 0 ? parsed["teamCollectorUrl"] : void 0;
|
|
32212
|
+
const teamPrivacyTier = ensurePrivacyTier(parsed["teamPrivacyTier"]) ? parsed["teamPrivacyTier"] : void 0;
|
|
32213
|
+
const teamAuthToken = typeof parsed["teamAuthToken"] === "string" && parsed["teamAuthToken"].length > 0 ? parsed["teamAuthToken"] : void 0;
|
|
29927
32214
|
return {
|
|
29928
32215
|
version: "1.0",
|
|
29929
32216
|
collectorUrl: parsed["collectorUrl"],
|
|
29930
32217
|
privacyTier: parsed["privacyTier"],
|
|
29931
32218
|
hookCommand: parsed["hookCommand"],
|
|
29932
|
-
updatedAt: parsed["updatedAt"]
|
|
32219
|
+
updatedAt: parsed["updatedAt"],
|
|
32220
|
+
...userEmail !== void 0 ? { userEmail } : {},
|
|
32221
|
+
...userName !== void 0 ? { userName } : {},
|
|
32222
|
+
...teamCollectorUrl !== void 0 ? { teamCollectorUrl } : {},
|
|
32223
|
+
...teamPrivacyTier !== void 0 ? { teamPrivacyTier } : {},
|
|
32224
|
+
...teamAuthToken !== void 0 ? { teamAuthToken } : {}
|
|
29933
32225
|
};
|
|
29934
32226
|
}
|
|
29935
32227
|
function isRecord2(value) {
|
|
@@ -30157,6 +32449,17 @@ function deriveOtelLogsEndpoint(collectorUrl) {
|
|
|
30157
32449
|
return "http://127.0.0.1:4717";
|
|
30158
32450
|
}
|
|
30159
32451
|
}
|
|
32452
|
+
function readGitConfig(key) {
|
|
32453
|
+
try {
|
|
32454
|
+
const output = (0, import_node_child_process.execFileSync)("git", ["config", "--global", key], {
|
|
32455
|
+
encoding: "utf8",
|
|
32456
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
32457
|
+
}).trim();
|
|
32458
|
+
return output.length > 0 ? output : void 0;
|
|
32459
|
+
} catch {
|
|
32460
|
+
return void 0;
|
|
32461
|
+
}
|
|
32462
|
+
}
|
|
30160
32463
|
function buildTelemetryEnv(collectorUrl, privacyTier) {
|
|
30161
32464
|
const enablePromptAndToolDetail = privacyTier >= 2;
|
|
30162
32465
|
const otelLogsEndpoint = deriveOtelLogsEndpoint(collectorUrl);
|
|
@@ -30173,12 +32476,19 @@ function buildTelemetryEnv(collectorUrl, privacyTier) {
|
|
|
30173
32476
|
}
|
|
30174
32477
|
function runInit(input, store = new FileCliConfigStore()) {
|
|
30175
32478
|
const timestamp = nowIso(input.nowIso);
|
|
32479
|
+
const userEmail = readGitConfig("user.email");
|
|
32480
|
+
const userName = readGitConfig("user.name");
|
|
30176
32481
|
const config = {
|
|
30177
32482
|
version: "1.0",
|
|
30178
32483
|
collectorUrl: input.collectorUrl ?? "http://127.0.0.1:8317/v1/hooks",
|
|
30179
32484
|
privacyTier: ensurePrivacyTierOrDefault(input.privacyTier),
|
|
30180
32485
|
hookCommand: "agent-trace hook-handler --forward",
|
|
30181
|
-
updatedAt: timestamp
|
|
32486
|
+
updatedAt: timestamp,
|
|
32487
|
+
...userEmail !== void 0 ? { userEmail } : {},
|
|
32488
|
+
...userName !== void 0 ? { userName } : {},
|
|
32489
|
+
...input.teamUrl !== void 0 ? { teamCollectorUrl: input.teamUrl } : {},
|
|
32490
|
+
...input.teamPrivacyTier !== void 0 ? { teamPrivacyTier: input.teamPrivacyTier } : {},
|
|
32491
|
+
...input.teamToken !== void 0 ? { teamAuthToken: input.teamToken } : {}
|
|
30182
32492
|
};
|
|
30183
32493
|
const telemetryEnv = buildTelemetryEnv(config.collectorUrl, config.privacyTier);
|
|
30184
32494
|
const hooks = buildClaudeHookConfig(config.hookCommand, timestamp);
|
|
@@ -30228,7 +32538,7 @@ function runStatus(configDir, store = new FileCliConfigStore()) {
|
|
|
30228
32538
|
|
|
30229
32539
|
// packages/cli/src/hook-handler.ts
|
|
30230
32540
|
var import_node_crypto4 = __toESM(require("node:crypto"));
|
|
30231
|
-
var
|
|
32541
|
+
var import_node_child_process2 = require("node:child_process");
|
|
30232
32542
|
var import_node_fs4 = __toESM(require("node:fs"));
|
|
30233
32543
|
var import_node_path4 = __toESM(require("node:path"));
|
|
30234
32544
|
function isIsoDate2(value) {
|
|
@@ -30661,7 +32971,7 @@ var FileHookSessionBaselineStore = class {
|
|
|
30661
32971
|
};
|
|
30662
32972
|
function runGitCommand(args, repositoryPath) {
|
|
30663
32973
|
try {
|
|
30664
|
-
const output = (0,
|
|
32974
|
+
const output = (0, import_node_child_process2.execFileSync)("git", [...args], {
|
|
30665
32975
|
...repositoryPath !== void 0 ? { cwd: repositoryPath } : {},
|
|
30666
32976
|
encoding: "utf8",
|
|
30667
32977
|
stdio: ["ignore", "pipe", "ignore"]
|
|
@@ -30889,9 +33199,23 @@ function runHookHandler(input, store = new FileCliConfigStore(), gitContextProvi
|
|
|
30889
33199
|
}
|
|
30890
33200
|
const now = input.nowIso ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
30891
33201
|
const privacyTier = getPrivacyTier(store, input.configDir);
|
|
33202
|
+
const config = store.readConfig(input.configDir);
|
|
30892
33203
|
const baselineStore = new FileHookSessionBaselineStore(store, input.configDir);
|
|
30893
33204
|
const enrichment = enrichHookPayloadWithGitContext(payload, gitContextProvider, baselineStore, now);
|
|
30894
|
-
|
|
33205
|
+
let enrichedPayload = enrichment.payload;
|
|
33206
|
+
if (config?.userEmail !== void 0) {
|
|
33207
|
+
const record = enrichedPayload;
|
|
33208
|
+
if (record["user_email"] === void 0) {
|
|
33209
|
+
enrichedPayload = { ...enrichedPayload, user_email: config.userEmail, user_id: config.userEmail };
|
|
33210
|
+
}
|
|
33211
|
+
}
|
|
33212
|
+
if (config?.userName !== void 0) {
|
|
33213
|
+
const record = enrichedPayload;
|
|
33214
|
+
if (record["user_name"] === void 0) {
|
|
33215
|
+
enrichedPayload = { ...enrichedPayload, user_name: config.userName };
|
|
33216
|
+
}
|
|
33217
|
+
}
|
|
33218
|
+
const envelope = toEnvelope(enrichedPayload, privacyTier, now, {
|
|
30895
33219
|
...enrichment.enriched ? { git_enriched: "1" } : {},
|
|
30896
33220
|
...enrichment.usedSessionBaselineDelta ? { git_session_delta: "1" } : {}
|
|
30897
33221
|
});
|
|
@@ -30907,14 +33231,64 @@ function runHookHandler(input, store = new FileCliConfigStore(), gitContextProvi
|
|
|
30907
33231
|
envelope
|
|
30908
33232
|
};
|
|
30909
33233
|
}
|
|
33234
|
+
function redactForPrivacy(envelope, targetTier) {
|
|
33235
|
+
if (targetTier >= 3) {
|
|
33236
|
+
return envelope;
|
|
33237
|
+
}
|
|
33238
|
+
const payload = envelope.payload;
|
|
33239
|
+
const redacted = { ...payload };
|
|
33240
|
+
if (targetTier <= 1) {
|
|
33241
|
+
const tier1StripKeys = [
|
|
33242
|
+
"tool_input",
|
|
33243
|
+
"toolInput",
|
|
33244
|
+
"tool_response",
|
|
33245
|
+
"toolResponse",
|
|
33246
|
+
"prompt_text",
|
|
33247
|
+
"promptText",
|
|
33248
|
+
"command",
|
|
33249
|
+
"bash_command",
|
|
33250
|
+
"bashCommand",
|
|
33251
|
+
"stdout",
|
|
33252
|
+
"output",
|
|
33253
|
+
"commit_message",
|
|
33254
|
+
"commitMessage",
|
|
33255
|
+
"last_assistant_message",
|
|
33256
|
+
"lastAssistantMessage",
|
|
33257
|
+
"response_text",
|
|
33258
|
+
"responseText"
|
|
33259
|
+
];
|
|
33260
|
+
for (const key of tier1StripKeys) {
|
|
33261
|
+
delete redacted[key];
|
|
33262
|
+
}
|
|
33263
|
+
} else if (targetTier === 2) {
|
|
33264
|
+
const tier2StripKeys = [
|
|
33265
|
+
"prompt_text",
|
|
33266
|
+
"promptText",
|
|
33267
|
+
"last_assistant_message",
|
|
33268
|
+
"lastAssistantMessage",
|
|
33269
|
+
"response_text",
|
|
33270
|
+
"responseText"
|
|
33271
|
+
];
|
|
33272
|
+
for (const key of tier2StripKeys) {
|
|
33273
|
+
delete redacted[key];
|
|
33274
|
+
}
|
|
33275
|
+
}
|
|
33276
|
+
return {
|
|
33277
|
+
...envelope,
|
|
33278
|
+
privacyTier: targetTier,
|
|
33279
|
+
payload: redacted
|
|
33280
|
+
};
|
|
33281
|
+
}
|
|
30910
33282
|
var FetchCollectorHttpClient = class {
|
|
30911
|
-
async postJson(url, payload) {
|
|
33283
|
+
async postJson(url, payload, extraHeaders) {
|
|
30912
33284
|
try {
|
|
33285
|
+
const headers = {
|
|
33286
|
+
"Content-Type": "application/json",
|
|
33287
|
+
...extraHeaders
|
|
33288
|
+
};
|
|
30913
33289
|
const response = await fetch(url, {
|
|
30914
33290
|
method: "POST",
|
|
30915
|
-
headers
|
|
30916
|
-
"Content-Type": "application/json"
|
|
30917
|
-
},
|
|
33291
|
+
headers,
|
|
30918
33292
|
body: JSON.stringify(payload)
|
|
30919
33293
|
});
|
|
30920
33294
|
const body = await response.text();
|
|
@@ -30936,6 +33310,17 @@ var FetchCollectorHttpClient = class {
|
|
|
30936
33310
|
function isSuccessStatus(statusCode) {
|
|
30937
33311
|
return statusCode >= 200 && statusCode < 300;
|
|
30938
33312
|
}
|
|
33313
|
+
function getTeamConfig(store, configDir) {
|
|
33314
|
+
const config = store.readConfig(configDir);
|
|
33315
|
+
if (config === void 0) {
|
|
33316
|
+
return {};
|
|
33317
|
+
}
|
|
33318
|
+
return {
|
|
33319
|
+
...config.teamCollectorUrl !== void 0 ? { teamCollectorUrl: config.teamCollectorUrl } : {},
|
|
33320
|
+
...config.teamPrivacyTier !== void 0 ? { teamPrivacyTier: config.teamPrivacyTier } : {},
|
|
33321
|
+
...config.teamAuthToken !== void 0 ? { teamAuthToken: config.teamAuthToken } : {}
|
|
33322
|
+
};
|
|
33323
|
+
}
|
|
30939
33324
|
async function runHookHandlerAndForward(input, client = new FetchCollectorHttpClient(), store = new FileCliConfigStore(), gitContextProvider = new ShellHookGitContextProvider()) {
|
|
30940
33325
|
const hookResult = runHookHandler(
|
|
30941
33326
|
{
|
|
@@ -30953,7 +33338,36 @@ async function runHookHandlerAndForward(input, client = new FetchCollectorHttpCl
|
|
|
30953
33338
|
};
|
|
30954
33339
|
}
|
|
30955
33340
|
const collectorUrl = getCollectorUrl(store, input.configDir, input.collectorUrl);
|
|
30956
|
-
const
|
|
33341
|
+
const teamConfig = getTeamConfig(store, input.configDir);
|
|
33342
|
+
const localPost = client.postJson(collectorUrl, hookResult.envelope);
|
|
33343
|
+
let teamPostPromise = Promise.resolve(void 0);
|
|
33344
|
+
if (teamConfig.teamCollectorUrl !== void 0 && teamConfig.teamCollectorUrl.length > 0) {
|
|
33345
|
+
const teamTier = teamConfig.teamPrivacyTier ?? 1;
|
|
33346
|
+
const redactedEnvelope = redactForPrivacy(hookResult.envelope, teamTier);
|
|
33347
|
+
const teamHeaders = {};
|
|
33348
|
+
if (teamConfig.teamAuthToken !== void 0 && teamConfig.teamAuthToken.length > 0) {
|
|
33349
|
+
teamHeaders["Authorization"] = `Bearer ${teamConfig.teamAuthToken}`;
|
|
33350
|
+
}
|
|
33351
|
+
teamPostPromise = client.postJson(teamConfig.teamCollectorUrl, redactedEnvelope, teamHeaders).catch((error) => ({
|
|
33352
|
+
ok: false,
|
|
33353
|
+
statusCode: 0,
|
|
33354
|
+
body: "",
|
|
33355
|
+
error: String(error)
|
|
33356
|
+
}));
|
|
33357
|
+
}
|
|
33358
|
+
const [postResult, teamPostResult] = await Promise.all([localPost, teamPostPromise]);
|
|
33359
|
+
let teamForwardError;
|
|
33360
|
+
if (teamPostResult !== void 0) {
|
|
33361
|
+
if (!teamPostResult.ok) {
|
|
33362
|
+
teamForwardError = teamPostResult.error ?? "failed to send to team collector";
|
|
33363
|
+
} else if (!isSuccessStatus(teamPostResult.statusCode)) {
|
|
33364
|
+
teamForwardError = `team collector returned status ${String(teamPostResult.statusCode)}`;
|
|
33365
|
+
}
|
|
33366
|
+
if (teamForwardError !== void 0) {
|
|
33367
|
+
process.stderr.write(`[agent-trace] team forward warning: ${teamForwardError}
|
|
33368
|
+
`);
|
|
33369
|
+
}
|
|
33370
|
+
}
|
|
30957
33371
|
if (!postResult.ok) {
|
|
30958
33372
|
return {
|
|
30959
33373
|
ok: false,
|
|
@@ -30977,7 +33391,8 @@ async function runHookHandlerAndForward(input, client = new FetchCollectorHttpCl
|
|
|
30977
33391
|
envelope: hookResult.envelope,
|
|
30978
33392
|
collectorUrl,
|
|
30979
33393
|
statusCode: postResult.statusCode,
|
|
30980
|
-
body: postResult.body
|
|
33394
|
+
body: postResult.body,
|
|
33395
|
+
...teamForwardError !== void 0 ? { teamForwardError } : {}
|
|
30981
33396
|
};
|
|
30982
33397
|
}
|
|
30983
33398
|
|
|
@@ -31102,13 +33517,17 @@ async function startServer() {
|
|
|
31102
33517
|
throw error;
|
|
31103
33518
|
}
|
|
31104
33519
|
const apiBaseUrl = `http://${host === "0.0.0.0" ? "127.0.0.1" : host}:${String(apiPort)}`;
|
|
33520
|
+
const cliConfigStore = new FileCliConfigStore();
|
|
33521
|
+
const cliConfig = cliConfigStore.readConfig();
|
|
33522
|
+
const currentUserEmail = cliConfig?.userEmail;
|
|
31105
33523
|
let dashboardAddress;
|
|
31106
33524
|
try {
|
|
31107
33525
|
const dashboard = await startDashboardServer({
|
|
31108
33526
|
host,
|
|
31109
33527
|
port: dashboardPort,
|
|
31110
33528
|
apiBaseUrl,
|
|
31111
|
-
startedAtMs
|
|
33529
|
+
startedAtMs,
|
|
33530
|
+
...currentUserEmail !== void 0 ? { currentUserEmail } : {}
|
|
31112
33531
|
});
|
|
31113
33532
|
dashboardAddress = dashboard.address;
|
|
31114
33533
|
const originalClose = servers.close;
|