agent-trace 0.2.12 → 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 +3304 -76
- 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>
|
|
@@ -24130,6 +24131,41 @@ th{color:var(--text-dim);font-size:10px;text-transform:uppercase;letter-spacing:
|
|
|
24130
24131
|
.pr-repo{color:var(--text-muted)}
|
|
24131
24132
|
.pr-link{color:var(--text-dim);text-decoration:none;font-size:11px}
|
|
24132
24133
|
.pr-link:hover{color:var(--cyan);text-decoration:underline}
|
|
24134
|
+
.settings-btn{position:absolute;right:16px;top:16px;background:var(--panel-muted);border:1px solid var(--panel-border);border-radius:6px;padding:6px 10px;cursor:pointer;color:var(--text-muted);font-size:12px;font-family:inherit;display:flex;align-items:center;gap:4px;transition:border-color .15s,color .15s}
|
|
24135
|
+
.settings-btn:hover{border-color:var(--green);color:var(--green)}
|
|
24136
|
+
.settings-btn svg{width:14px;height:14px}
|
|
24137
|
+
.modal-overlay{display:none;position:fixed;inset:0;background:rgba(0,0,0,.7);z-index:1000;justify-content:center;align-items:center}
|
|
24138
|
+
.modal-overlay.open{display:flex}
|
|
24139
|
+
.modal{background:var(--panel);border:1px solid var(--panel-border);border-radius:10px;padding:20px;width:420px;max-width:90vw;position:relative}
|
|
24140
|
+
.modal h3{margin:0 0 16px;font-size:14px;font-weight:600;color:var(--text-primary)}
|
|
24141
|
+
.modal-close{position:absolute;right:12px;top:12px;background:none;border:none;color:var(--text-dim);font-size:18px;cursor:pointer;padding:4px 8px;border-radius:4px}
|
|
24142
|
+
.modal-close:hover{color:var(--text-primary);background:var(--panel-hover)}
|
|
24143
|
+
.modal label{display:block;font-size:11px;text-transform:uppercase;letter-spacing:.08em;color:var(--text-dim);margin-bottom:4px;margin-top:12px}
|
|
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}
|
|
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}
|
|
24147
|
+
.modal-actions{margin-top:16px;display:flex;gap:8px;align-items:center}
|
|
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}
|
|
24149
|
+
.modal-save:hover{opacity:.9}
|
|
24150
|
+
.modal-save:disabled{opacity:.5;cursor:not-allowed}
|
|
24151
|
+
.modal-status{font-size:11px;color:var(--text-muted);flex:1}
|
|
24152
|
+
.modal-status.error{color:var(--red)}
|
|
24153
|
+
.modal-status.ok{color:var(--green)}
|
|
24154
|
+
.insight-panel{border:1px solid rgba(192,132,252,.2);border-radius:8px;padding:12px;margin-bottom:12px;background:rgba(192,132,252,.04)}
|
|
24155
|
+
.insight-hd{display:flex;align-items:center;justify-content:space-between;margin-bottom:8px}
|
|
24156
|
+
.insight-title{font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.06em;color:var(--purple)}
|
|
24157
|
+
.insight-meta{font-size:10px;color:var(--text-dim)}
|
|
24158
|
+
.insight-summary{font-size:12px;color:var(--text-primary);line-height:1.6;margin-bottom:8px}
|
|
24159
|
+
.insight-section{margin-bottom:6px}
|
|
24160
|
+
.insight-section-title{font-size:10px;text-transform:uppercase;letter-spacing:.06em;color:var(--text-dim);margin-bottom:4px}
|
|
24161
|
+
.insight-item{font-size:12px;color:var(--text-muted);line-height:1.5;padding:2px 0 2px 12px;position:relative}
|
|
24162
|
+
.insight-item::before{content:'>';position:absolute;left:0;color:var(--purple)}
|
|
24163
|
+
.insight-cost{font-size:11px;color:var(--orange);margin-top:4px}
|
|
24164
|
+
.insight-gen-btn{padding:6px 14px;background:var(--purple-dim);color:var(--purple);border:1px solid rgba(192,132,252,.3);border-radius:6px;font-size:12px;cursor:pointer;font-family:inherit;transition:background .15s}
|
|
24165
|
+
.insight-gen-btn:hover{background:rgba(192,132,252,.15)}
|
|
24166
|
+
.insight-gen-btn:disabled{opacity:.5;cursor:not-allowed}
|
|
24167
|
+
.insight-loading{font-size:12px;color:var(--text-muted);padding:8px 0}
|
|
24168
|
+
.insight-error{font-size:12px;color:var(--red);padding:8px 0}
|
|
24133
24169
|
.pcommits{border:1px solid rgba(250,204,21,.15);border-radius:4px;padding:6px 8px;margin-bottom:8px;background:rgba(250,204,21,.04)}
|
|
24134
24170
|
.pcommit{display:flex;align-items:center;gap:8px;padding:2px 0;font-size:12px}
|
|
24135
24171
|
.hljs{background:transparent!important;color:var(--text-primary)}
|
|
@@ -24146,17 +24182,138 @@ th{color:var(--text-dim);font-size:10px;text-transform:uppercase;letter-spacing:
|
|
|
24146
24182
|
.hljs-property{color:#93c5fd}
|
|
24147
24183
|
.hljs-addition{color:#4ade80;background:rgba(74,222,128,.08)}
|
|
24148
24184
|
.hljs-deletion{color:#f87171;background:rgba(248,113,113,.08)}
|
|
24149
|
-
|
|
24150
|
-
|
|
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}}
|
|
24151
24271
|
</style>
|
|
24152
24272
|
</head>
|
|
24153
24273
|
<body>
|
|
24154
24274
|
<main class="shell">
|
|
24155
|
-
<section class="hero">
|
|
24275
|
+
<section class="hero" style="position:relative">
|
|
24156
24276
|
<h1>${title}</h1>
|
|
24157
24277
|
<p>session observability for coding agents</p>
|
|
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>
|
|
24158
24284
|
<div id="status" class="status-banner">Connecting...</div>
|
|
24159
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>
|
|
24294
|
+
<div id="settings-modal" class="modal-overlay" onclick="if(event.target===this)closeSettings()">
|
|
24295
|
+
<div class="modal">
|
|
24296
|
+
<button class="modal-close" onclick="closeSettings()">×</button>
|
|
24297
|
+
<h3>AI Insights Settings</h3>
|
|
24298
|
+
<p style="font-size:11px;color:var(--text-muted);margin:0 0 8px">Configure your own API key to generate AI-powered session insights.</p>
|
|
24299
|
+
<label>Provider</label>
|
|
24300
|
+
<select id="cfg-provider">
|
|
24301
|
+
<option value="anthropic">Anthropic</option>
|
|
24302
|
+
<option value="openai">OpenAI</option>
|
|
24303
|
+
<option value="gemini">Gemini</option>
|
|
24304
|
+
<option value="openrouter">OpenRouter</option>
|
|
24305
|
+
</select>
|
|
24306
|
+
<label>API Key</label>
|
|
24307
|
+
<input type="password" id="cfg-apikey" placeholder="sk-..." autocomplete="off"/>
|
|
24308
|
+
<label>Model (optional)</label>
|
|
24309
|
+
<input type="text" id="cfg-model" placeholder="leave blank for default"/>
|
|
24310
|
+
<div class="modal-actions">
|
|
24311
|
+
<button class="modal-save" id="cfg-save" onclick="saveSettings()">Save</button>
|
|
24312
|
+
<span class="modal-status" id="cfg-status"></span>
|
|
24313
|
+
</div>
|
|
24314
|
+
</div>
|
|
24315
|
+
</div>
|
|
24316
|
+
<div id="tab-sessions" class="tab-content active">
|
|
24160
24317
|
<section class="mg">
|
|
24161
24318
|
<article class="mc"><div class="label">Sessions</div><div class="val green" id="m-sessions">0</div></article>
|
|
24162
24319
|
<article class="mc"><div class="label">Total Cost</div><div class="val orange" id="m-cost">$0.00</div></article>
|
|
@@ -24166,7 +24323,7 @@ th{color:var(--text-dim);font-size:10px;text-transform:uppercase;letter-spacing:
|
|
|
24166
24323
|
</section>
|
|
24167
24324
|
<section class="sg">
|
|
24168
24325
|
<section class="panel">
|
|
24169
|
-
<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>
|
|
24170
24327
|
<div class="pc"><div id="sessions-area" class="empty">Loading...</div></div>
|
|
24171
24328
|
</section>
|
|
24172
24329
|
<section class="panel">
|
|
@@ -24178,6 +24335,119 @@ th{color:var(--text-dim);font-size:10px;text-transform:uppercase;letter-spacing:
|
|
|
24178
24335
|
<header class="ph"><div><h2>Session Replay</h2><p id="replay-label">select a session</p></div></header>
|
|
24179
24336
|
<div class="pc" id="replay-area"><div class="empty">No session selected.</div></div>
|
|
24180
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>
|
|
24181
24451
|
</main>
|
|
24182
24452
|
<script>
|
|
24183
24453
|
(function(){
|
|
@@ -24186,6 +24456,402 @@ var selectedId = null;
|
|
|
24186
24456
|
var sessions = [];
|
|
24187
24457
|
var costPoints = [];
|
|
24188
24458
|
var replay = null;
|
|
24459
|
+
var insightsConfigured = false;
|
|
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
|
+
};
|
|
24771
|
+
|
|
24772
|
+
window.openSettings = function() {
|
|
24773
|
+
document.getElementById('settings-modal').classList.add('open');
|
|
24774
|
+
fetch('/api/settings/insights',{cache:'no-store'}).then(function(r){return r.json();}).then(function(data){
|
|
24775
|
+
if(data && data.configured){
|
|
24776
|
+
document.getElementById('cfg-provider').value = data.provider || 'anthropic';
|
|
24777
|
+
if(data.model) document.getElementById('cfg-model').value = data.model;
|
|
24778
|
+
document.getElementById('cfg-status').className = 'modal-status ok';
|
|
24779
|
+
document.getElementById('cfg-status').textContent = 'Configured (' + (data.provider||'') + ')';
|
|
24780
|
+
insightsConfigured = true;
|
|
24781
|
+
}
|
|
24782
|
+
}).catch(function(){});
|
|
24783
|
+
};
|
|
24784
|
+
|
|
24785
|
+
window.closeSettings = function() {
|
|
24786
|
+
document.getElementById('settings-modal').classList.remove('open');
|
|
24787
|
+
};
|
|
24788
|
+
|
|
24789
|
+
window.saveSettings = function() {
|
|
24790
|
+
var btn = document.getElementById('cfg-save');
|
|
24791
|
+
var status = document.getElementById('cfg-status');
|
|
24792
|
+
btn.disabled = true;
|
|
24793
|
+
status.className = 'modal-status';
|
|
24794
|
+
status.textContent = 'Validating...';
|
|
24795
|
+
var body = {
|
|
24796
|
+
provider: document.getElementById('cfg-provider').value,
|
|
24797
|
+
apiKey: document.getElementById('cfg-apikey').value,
|
|
24798
|
+
model: document.getElementById('cfg-model').value || undefined
|
|
24799
|
+
};
|
|
24800
|
+
fetch('/api/settings/insights',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)})
|
|
24801
|
+
.then(function(r){return r.json().then(function(d){return{ok:r.ok,data:d};});})
|
|
24802
|
+
.then(function(res){
|
|
24803
|
+
btn.disabled = false;
|
|
24804
|
+
if(res.ok && res.data.status === 'ok'){
|
|
24805
|
+
status.className = 'modal-status ok';
|
|
24806
|
+
status.textContent = 'Saved! (' + (res.data.provider||'') + ' / ' + (res.data.model||'default') + ')';
|
|
24807
|
+
insightsConfigured = true;
|
|
24808
|
+
document.getElementById('cfg-apikey').value = '';
|
|
24809
|
+
if(replay) renderReplay();
|
|
24810
|
+
} else {
|
|
24811
|
+
status.className = 'modal-status error';
|
|
24812
|
+
status.textContent = res.data.message || 'Save failed';
|
|
24813
|
+
}
|
|
24814
|
+
}).catch(function(e){
|
|
24815
|
+
btn.disabled = false;
|
|
24816
|
+
status.className = 'modal-status error';
|
|
24817
|
+
status.textContent = String(e);
|
|
24818
|
+
});
|
|
24819
|
+
};
|
|
24820
|
+
|
|
24821
|
+
window.generateInsight = function(sid) {
|
|
24822
|
+
var panel = document.getElementById('insight-panel');
|
|
24823
|
+
if(!panel) return;
|
|
24824
|
+
panel.innerHTML = '<div class="insight-loading">Generating insight...</div>';
|
|
24825
|
+
fetch('/api/session/' + encodeURIComponent(sid) + '/insights',{method:'POST',headers:{'Content-Type':'application/json'},body:'{}'})
|
|
24826
|
+
.then(function(r){return r.json().then(function(d){return{ok:r.ok,data:d};});})
|
|
24827
|
+
.then(function(res){
|
|
24828
|
+
if(res.ok && res.data.status === 'ok' && res.data.insight){
|
|
24829
|
+
insightsCache[sid] = res.data.insight;
|
|
24830
|
+
renderInsightContent(panel, res.data.insight);
|
|
24831
|
+
} else {
|
|
24832
|
+
panel.innerHTML = '<div class="insight-error">' + esc(res.data.message || 'Failed to generate insight') + '</div>';
|
|
24833
|
+
}
|
|
24834
|
+
}).catch(function(e){
|
|
24835
|
+
panel.innerHTML = '<div class="insight-error">' + esc(String(e)) + '</div>';
|
|
24836
|
+
});
|
|
24837
|
+
};
|
|
24838
|
+
|
|
24839
|
+
function renderInsightContent(panel, insight) {
|
|
24840
|
+
var h = '<div class="insight-hd"><span class="insight-title">AI Insight</span><span class="insight-meta">' + esc(insight.provider||'') + ' / ' + esc(insight.model||'') + '</span></div>';
|
|
24841
|
+
h += '<div class="insight-summary">' + esc(insight.summary) + '</div>';
|
|
24842
|
+
if(insight.highlights && insight.highlights.length > 0){
|
|
24843
|
+
h += '<div class="insight-section"><div class="insight-section-title">Highlights</div>';
|
|
24844
|
+
insight.highlights.forEach(function(item){ h += '<div class="insight-item">' + esc(item) + '</div>'; });
|
|
24845
|
+
h += '</div>';
|
|
24846
|
+
}
|
|
24847
|
+
if(insight.suggestions && insight.suggestions.length > 0){
|
|
24848
|
+
h += '<div class="insight-section"><div class="insight-section-title">Suggestions</div>';
|
|
24849
|
+
insight.suggestions.forEach(function(item){ h += '<div class="insight-item">' + esc(item) + '</div>'; });
|
|
24850
|
+
h += '</div>';
|
|
24851
|
+
}
|
|
24852
|
+
if(insight.costNote) h += '<div class="insight-cost">' + esc(insight.costNote) + '</div>';
|
|
24853
|
+
panel.innerHTML = h;
|
|
24854
|
+
}
|
|
24189
24855
|
|
|
24190
24856
|
function esc(s) {
|
|
24191
24857
|
return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(new RegExp(DQ,'g'),'"');
|
|
@@ -24402,9 +25068,16 @@ window.togglePrompt = function(hd) {
|
|
|
24402
25068
|
|
|
24403
25069
|
function renderSessions() {
|
|
24404
25070
|
var area = document.getElementById('sessions-area');
|
|
24405
|
-
|
|
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; }
|
|
24406
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>';
|
|
24407
|
-
|
|
25080
|
+
filtered.forEach(function(s){
|
|
24408
25081
|
var active = s.sessionId === selectedId ? ' active' : '';
|
|
24409
25082
|
var repo = s.gitRepo ? (s.gitBranch ? s.gitRepo + '/' + s.gitBranch : s.gitRepo) : '-';
|
|
24410
25083
|
var commits = s.commitCount > 0 ? '<span class="badge green">' + s.commitCount + '</span>' : '<span class="badge dim">0</span>';
|
|
@@ -24416,14 +25089,15 @@ function renderSessions() {
|
|
|
24416
25089
|
}
|
|
24417
25090
|
|
|
24418
25091
|
function renderMetrics() {
|
|
24419
|
-
|
|
24420
|
-
document.getElementById('m-
|
|
24421
|
-
document.getElementById('m-
|
|
24422
|
-
document.getElementById('m-
|
|
24423
|
-
|
|
24424
|
-
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;
|
|
24425
25099
|
document.getElementById('m-commits').textContent = tc;
|
|
24426
|
-
document.getElementById('m-commits-det').textContent = sc + '/' +
|
|
25100
|
+
document.getElementById('m-commits-det').textContent = sc + '/' + my.length + ' sessions produced commits';
|
|
24427
25101
|
}
|
|
24428
25102
|
|
|
24429
25103
|
function renderCostChart() {
|
|
@@ -24479,6 +25153,17 @@ function renderReplay() {
|
|
|
24479
25153
|
}
|
|
24480
25154
|
h += '</div>';
|
|
24481
25155
|
}
|
|
25156
|
+
// AI insight panel
|
|
25157
|
+
h += '<div class="insight-panel" id="insight-panel">';
|
|
25158
|
+
var cachedInsight = insightsCache[replay.sessionId];
|
|
25159
|
+
if (cachedInsight) {
|
|
25160
|
+
// will be rendered after innerHTML set
|
|
25161
|
+
} else if (insightsConfigured) {
|
|
25162
|
+
h += '<div style="display:flex;align-items:center;justify-content:space-between"><span class="insight-title">AI Insight</span><button class="insight-gen-btn" data-sid="' + esc(replay.sessionId) + '">Generate Insight</button></div>';
|
|
25163
|
+
} else {
|
|
25164
|
+
h += '<div style="display:flex;align-items:center;justify-content:space-between"><span class="insight-title">AI Insight</span><span style="font-size:11px;color:var(--text-dim)">Configure an API key in settings to enable AI insights</span></div>';
|
|
25165
|
+
}
|
|
25166
|
+
h += '</div>';
|
|
24482
25167
|
// prompt groups
|
|
24483
25168
|
var groups = buildPromptGroups(replay.timeline, replay.commits);
|
|
24484
25169
|
if (groups.length === 0) {
|
|
@@ -24487,6 +25172,14 @@ function renderReplay() {
|
|
|
24487
25172
|
groups.forEach(function(g, i) { h += renderPromptCard(g, i + 1); });
|
|
24488
25173
|
}
|
|
24489
25174
|
area.innerHTML = h;
|
|
25175
|
+
if (cachedInsight) {
|
|
25176
|
+
var ip = document.getElementById('insight-panel');
|
|
25177
|
+
if (ip) renderInsightContent(ip, cachedInsight);
|
|
25178
|
+
}
|
|
25179
|
+
var genBtn = area.querySelector('.insight-gen-btn[data-sid]');
|
|
25180
|
+
if (genBtn) {
|
|
25181
|
+
genBtn.addEventListener('click', function() { generateInsight(genBtn.getAttribute('data-sid')); });
|
|
25182
|
+
}
|
|
24490
25183
|
}
|
|
24491
25184
|
|
|
24492
25185
|
function parseSummary(v) {
|
|
@@ -24508,8 +25201,10 @@ function setSessions(raw) {
|
|
|
24508
25201
|
sessions = sortLatest(raw.map(parseSummary).filter(Boolean));
|
|
24509
25202
|
renderMetrics();
|
|
24510
25203
|
renderSessions();
|
|
24511
|
-
if (
|
|
24512
|
-
|
|
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);
|
|
24513
25208
|
}
|
|
24514
25209
|
}
|
|
24515
25210
|
|
|
@@ -24554,7 +25249,10 @@ function loadSnapshot() {
|
|
|
24554
25249
|
});
|
|
24555
25250
|
}
|
|
24556
25251
|
|
|
24557
|
-
function
|
|
25252
|
+
function startStreaming() {
|
|
25253
|
+
fetch('/api/settings/insights',{cache:'no-store'}).then(function(r){return r.json();}).then(function(data){
|
|
25254
|
+
if(data && data.configured) insightsConfigured = true;
|
|
25255
|
+
}).catch(function(){});
|
|
24558
25256
|
loadSnapshot().then(function(){
|
|
24559
25257
|
if (typeof EventSource !== 'undefined') {
|
|
24560
25258
|
var es = new EventSource('/api/sessions/stream');
|
|
@@ -24565,6 +25263,12 @@ function boot() {
|
|
|
24565
25263
|
document.getElementById('stream-label').textContent = 'live';
|
|
24566
25264
|
document.getElementById('status').className = 'status-banner';
|
|
24567
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
|
+
}
|
|
24568
25272
|
}
|
|
24569
25273
|
});
|
|
24570
25274
|
es.addEventListener('bridge_error', function(event) {
|
|
@@ -24582,6 +25286,645 @@ function boot() {
|
|
|
24582
25286
|
});
|
|
24583
25287
|
}
|
|
24584
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
|
+
|
|
24585
25928
|
boot();
|
|
24586
25929
|
})();
|
|
24587
25930
|
</script>
|
|
@@ -24881,6 +26224,131 @@ async function startDashboardServer(options = {}) {
|
|
|
24881
26224
|
const pathname = parsePathname(url);
|
|
24882
26225
|
const segments = parsePathSegments(pathname);
|
|
24883
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
|
+
}
|
|
26318
|
+
if (method === "POST" && (pathname === "/api/settings/insights" || segments.length === 4 && segments[0] === "api" && segments[1] === "session" && segments[3] === "insights")) {
|
|
26319
|
+
let body = "";
|
|
26320
|
+
req.setEncoding("utf8");
|
|
26321
|
+
req.on("data", (chunk) => {
|
|
26322
|
+
body += chunk;
|
|
26323
|
+
});
|
|
26324
|
+
req.on("end", () => {
|
|
26325
|
+
let parsedBody;
|
|
26326
|
+
try {
|
|
26327
|
+
parsedBody = body.length > 0 ? JSON.parse(body) : {};
|
|
26328
|
+
} catch {
|
|
26329
|
+
sendJson(res, 400, { status: "error", message: "invalid JSON body" });
|
|
26330
|
+
return;
|
|
26331
|
+
}
|
|
26332
|
+
let apiPath;
|
|
26333
|
+
if (pathname === "/api/settings/insights") {
|
|
26334
|
+
apiPath = "/v1/settings/insights";
|
|
26335
|
+
} else {
|
|
26336
|
+
const sessionId = segments[2];
|
|
26337
|
+
apiPath = `/v1/sessions/${encodeURIComponent(sessionId ?? "")}/insights`;
|
|
26338
|
+
}
|
|
26339
|
+
void fetch(`${apiBaseUrl}${apiPath}`, {
|
|
26340
|
+
method: "POST",
|
|
26341
|
+
headers: { "Content-Type": "application/json" },
|
|
26342
|
+
body: JSON.stringify(parsedBody)
|
|
26343
|
+
}).then(async (apiResponse) => {
|
|
26344
|
+
const payload = await apiResponse.json();
|
|
26345
|
+
sendJson(res, apiResponse.status, payload);
|
|
26346
|
+
}).catch((error) => {
|
|
26347
|
+
sendJson(res, 502, { status: "error", message: `proxy error: ${String(error)}` });
|
|
26348
|
+
});
|
|
26349
|
+
});
|
|
26350
|
+
return;
|
|
26351
|
+
}
|
|
24884
26352
|
if (method !== "GET") {
|
|
24885
26353
|
sendJson(res, 405, {
|
|
24886
26354
|
status: "error",
|
|
@@ -24930,6 +26398,88 @@ async function startDashboardServer(options = {}) {
|
|
|
24930
26398
|
});
|
|
24931
26399
|
return;
|
|
24932
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
|
+
}
|
|
24933
26483
|
if (segments.length === 3 && segments[0] === "api" && segments[1] === "session") {
|
|
24934
26484
|
const encodedSessionId = segments[2];
|
|
24935
26485
|
let sessionId = "";
|
|
@@ -24972,8 +26522,17 @@ async function startDashboardServer(options = {}) {
|
|
|
24972
26522
|
});
|
|
24973
26523
|
return;
|
|
24974
26524
|
}
|
|
26525
|
+
if (pathname === "/api/settings/insights") {
|
|
26526
|
+
void fetch(`${apiBaseUrl}/v1/settings/insights`).then(async (apiResponse) => {
|
|
26527
|
+
const payload = await apiResponse.json();
|
|
26528
|
+
sendJson(res, apiResponse.status, payload);
|
|
26529
|
+
}).catch((error) => {
|
|
26530
|
+
sendJson(res, 502, { status: "error", message: `proxy error: ${String(error)}` });
|
|
26531
|
+
});
|
|
26532
|
+
return;
|
|
26533
|
+
}
|
|
24975
26534
|
if (pathname === "/") {
|
|
24976
|
-
sendHtml(res, 200, renderDashboardHtml());
|
|
26535
|
+
sendHtml(res, 200, renderDashboardHtml(options.currentUserEmail !== void 0 ? { currentUserEmail: options.currentUserEmail } : {}));
|
|
24977
26536
|
return;
|
|
24978
26537
|
}
|
|
24979
26538
|
sendJson(res, 404, {
|
|
@@ -25089,6 +26648,12 @@ CREATE TABLE IF NOT EXISTS pull_requests (
|
|
|
25089
26648
|
);
|
|
25090
26649
|
|
|
25091
26650
|
CREATE INDEX IF NOT EXISTS idx_pull_requests_session ON pull_requests(session_id);
|
|
26651
|
+
|
|
26652
|
+
CREATE TABLE IF NOT EXISTS instance_settings (
|
|
26653
|
+
key TEXT PRIMARY KEY,
|
|
26654
|
+
value TEXT NOT NULL,
|
|
26655
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
26656
|
+
);
|
|
25092
26657
|
`;
|
|
25093
26658
|
function toJsonArray(value) {
|
|
25094
26659
|
return JSON.stringify(value);
|
|
@@ -25127,6 +26692,7 @@ var SqliteClient = class {
|
|
|
25127
26692
|
this.migrateDeduplicateEvents();
|
|
25128
26693
|
this.db.exec(SCHEMA_SQL);
|
|
25129
26694
|
this.migrateRebuildBrokenTraces();
|
|
26695
|
+
this.migrateTeamColumns();
|
|
25130
26696
|
}
|
|
25131
26697
|
async insertJsonEachRow(request) {
|
|
25132
26698
|
if (request.rows.length === 0) return;
|
|
@@ -25138,18 +26704,20 @@ var SqliteClient = class {
|
|
|
25138
26704
|
if (rows.length === 0) return;
|
|
25139
26705
|
const upsert = this.db.prepare(`
|
|
25140
26706
|
INSERT INTO session_traces
|
|
25141
|
-
(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,
|
|
25142
26708
|
prompt_count, tool_call_count, api_call_count, total_cost_usd,
|
|
25143
26709
|
total_input_tokens, total_output_tokens, total_cache_read_tokens, total_cache_write_tokens,
|
|
25144
26710
|
lines_added, lines_removed,
|
|
25145
26711
|
models_used, tools_used, files_touched, commit_count, updated_at)
|
|
25146
26712
|
VALUES
|
|
25147
|
-
(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
26713
|
+
(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
25148
26714
|
ON CONFLICT(session_id) DO UPDATE SET
|
|
25149
26715
|
version = excluded.version,
|
|
25150
26716
|
started_at = excluded.started_at,
|
|
25151
26717
|
ended_at = excluded.ended_at,
|
|
25152
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),
|
|
25153
26721
|
git_repo = excluded.git_repo,
|
|
25154
26722
|
git_branch = excluded.git_branch,
|
|
25155
26723
|
prompt_count = excluded.prompt_count,
|
|
@@ -25176,6 +26744,8 @@ var SqliteClient = class {
|
|
|
25176
26744
|
row.started_at,
|
|
25177
26745
|
row.ended_at,
|
|
25178
26746
|
row.user_id,
|
|
26747
|
+
row.user_email ?? null,
|
|
26748
|
+
row.user_display_name ?? null,
|
|
25179
26749
|
row.git_repo,
|
|
25180
26750
|
row.git_branch,
|
|
25181
26751
|
row.prompt_count,
|
|
@@ -25382,6 +26952,47 @@ var SqliteClient = class {
|
|
|
25382
26952
|
sessionCount: Number(raw["sessions_count"] ?? 0)
|
|
25383
26953
|
}));
|
|
25384
26954
|
}
|
|
26955
|
+
getSetting(key) {
|
|
26956
|
+
const row = this.db.prepare(
|
|
26957
|
+
"SELECT value FROM instance_settings WHERE key = ?"
|
|
26958
|
+
).get(key);
|
|
26959
|
+
return row?.value;
|
|
26960
|
+
}
|
|
26961
|
+
upsertSetting(key, value) {
|
|
26962
|
+
this.db.prepare(`
|
|
26963
|
+
INSERT INTO instance_settings (key, value, updated_at)
|
|
26964
|
+
VALUES (?, ?, datetime('now'))
|
|
26965
|
+
ON CONFLICT(key) DO UPDATE SET
|
|
26966
|
+
value = excluded.value,
|
|
26967
|
+
updated_at = excluded.updated_at
|
|
26968
|
+
`).run(key, value);
|
|
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
|
+
}
|
|
25385
26996
|
close() {
|
|
25386
26997
|
this.db.close();
|
|
25387
26998
|
}
|
|
@@ -25510,8 +27121,44 @@ var SqliteClient = class {
|
|
|
25510
27121
|
if (broken === void 0) {
|
|
25511
27122
|
return;
|
|
25512
27123
|
}
|
|
25513
|
-
console.log("[agent-trace] migrating: rebuilding session traces with correct models/tools");
|
|
25514
|
-
this.rebuildSessionTracesFromEvents();
|
|
27124
|
+
console.log("[agent-trace] migrating: rebuilding session traces with correct models/tools");
|
|
27125
|
+
this.rebuildSessionTracesFromEvents();
|
|
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
|
+
`);
|
|
25515
27162
|
}
|
|
25516
27163
|
/**
|
|
25517
27164
|
* Rebuild session_traces by aggregating deduplicated agent_events.
|
|
@@ -26411,32 +28058,867 @@ function lookupModelPricing(model) {
|
|
|
26411
28058
|
return pricing;
|
|
26412
28059
|
}
|
|
26413
28060
|
}
|
|
26414
|
-
return void 0;
|
|
28061
|
+
return void 0;
|
|
28062
|
+
}
|
|
28063
|
+
function calculateCostUsd(input) {
|
|
28064
|
+
if (input.model === void 0) {
|
|
28065
|
+
return 0;
|
|
28066
|
+
}
|
|
28067
|
+
const pricing = lookupModelPricing(input.model);
|
|
28068
|
+
if (pricing === void 0) {
|
|
28069
|
+
return 0;
|
|
28070
|
+
}
|
|
28071
|
+
const baseInput = Math.max(0, input.inputTokens);
|
|
28072
|
+
const output = Math.max(0, input.outputTokens);
|
|
28073
|
+
const cacheRead = Math.max(0, input.cacheReadTokens ?? 0);
|
|
28074
|
+
const cacheWrite = Math.max(0, input.cacheWriteTokens ?? 0);
|
|
28075
|
+
const cost = baseInput * pricing.inputPerToken + cacheRead * pricing.cacheReadPerToken + cacheWrite * pricing.cacheWritePerToken + output * pricing.outputPerToken;
|
|
28076
|
+
return Number(cost.toFixed(6));
|
|
28077
|
+
}
|
|
28078
|
+
|
|
28079
|
+
// packages/runtime/src/runtime.ts
|
|
28080
|
+
var import_node_http2 = __toESM(require("node:http"));
|
|
28081
|
+
|
|
28082
|
+
// packages/api/src/insights-provider.ts
|
|
28083
|
+
var DEFAULT_MODELS = {
|
|
28084
|
+
anthropic: "claude-sonnet-4-20250514",
|
|
28085
|
+
openai: "gpt-4o-mini",
|
|
28086
|
+
gemini: "gemini-2.0-flash",
|
|
28087
|
+
openrouter: "anthropic/claude-sonnet-4"
|
|
28088
|
+
};
|
|
28089
|
+
function extractTextContent(body) {
|
|
28090
|
+
if (typeof body !== "object" || body === null) return "";
|
|
28091
|
+
const record = body;
|
|
28092
|
+
if (Array.isArray(record["content"])) {
|
|
28093
|
+
for (const block of record["content"]) {
|
|
28094
|
+
if (typeof block === "object" && block !== null) {
|
|
28095
|
+
const b = block;
|
|
28096
|
+
if (b["type"] === "text" && typeof b["text"] === "string") return b["text"];
|
|
28097
|
+
}
|
|
28098
|
+
}
|
|
28099
|
+
}
|
|
28100
|
+
if (Array.isArray(record["choices"])) {
|
|
28101
|
+
const first = record["choices"][0];
|
|
28102
|
+
if (typeof first === "object" && first !== null) {
|
|
28103
|
+
const choice = first;
|
|
28104
|
+
const msg = choice["message"];
|
|
28105
|
+
if (typeof msg === "object" && msg !== null) {
|
|
28106
|
+
const m = msg;
|
|
28107
|
+
if (typeof m["content"] === "string") return m["content"];
|
|
28108
|
+
}
|
|
28109
|
+
}
|
|
28110
|
+
}
|
|
28111
|
+
if (Array.isArray(record["candidates"])) {
|
|
28112
|
+
const first = record["candidates"][0];
|
|
28113
|
+
if (typeof first === "object" && first !== null) {
|
|
28114
|
+
const candidate = first;
|
|
28115
|
+
const content = candidate["content"];
|
|
28116
|
+
if (typeof content === "object" && content !== null) {
|
|
28117
|
+
const c = content;
|
|
28118
|
+
if (Array.isArray(c["parts"])) {
|
|
28119
|
+
for (const part of c["parts"]) {
|
|
28120
|
+
if (typeof part === "object" && part !== null) {
|
|
28121
|
+
const p = part;
|
|
28122
|
+
if (typeof p["text"] === "string") return p["text"];
|
|
28123
|
+
}
|
|
28124
|
+
}
|
|
28125
|
+
}
|
|
28126
|
+
}
|
|
28127
|
+
}
|
|
28128
|
+
}
|
|
28129
|
+
return "";
|
|
28130
|
+
}
|
|
28131
|
+
function createAnthropicProvider(apiKey, model) {
|
|
28132
|
+
const endpoint = "https://api.anthropic.com/v1/messages";
|
|
28133
|
+
return {
|
|
28134
|
+
provider: "anthropic",
|
|
28135
|
+
model,
|
|
28136
|
+
async complete(system, user, maxTokens) {
|
|
28137
|
+
const response = await fetch(endpoint, {
|
|
28138
|
+
method: "POST",
|
|
28139
|
+
headers: {
|
|
28140
|
+
"Content-Type": "application/json",
|
|
28141
|
+
"x-api-key": apiKey,
|
|
28142
|
+
"anthropic-version": "2023-06-01"
|
|
28143
|
+
},
|
|
28144
|
+
body: JSON.stringify({
|
|
28145
|
+
model,
|
|
28146
|
+
max_tokens: maxTokens ?? 1024,
|
|
28147
|
+
system,
|
|
28148
|
+
messages: [{ role: "user", content: user }]
|
|
28149
|
+
})
|
|
28150
|
+
});
|
|
28151
|
+
if (!response.ok) {
|
|
28152
|
+
throw new Error(`anthropic api returned ${String(response.status)}`);
|
|
28153
|
+
}
|
|
28154
|
+
const body = await response.json();
|
|
28155
|
+
return extractTextContent(body);
|
|
28156
|
+
},
|
|
28157
|
+
async validate() {
|
|
28158
|
+
try {
|
|
28159
|
+
const response = await fetch(endpoint, {
|
|
28160
|
+
method: "POST",
|
|
28161
|
+
headers: {
|
|
28162
|
+
"Content-Type": "application/json",
|
|
28163
|
+
"x-api-key": apiKey,
|
|
28164
|
+
"anthropic-version": "2023-06-01"
|
|
28165
|
+
},
|
|
28166
|
+
body: JSON.stringify({
|
|
28167
|
+
model,
|
|
28168
|
+
max_tokens: 1,
|
|
28169
|
+
messages: [{ role: "user", content: "hi" }]
|
|
28170
|
+
})
|
|
28171
|
+
});
|
|
28172
|
+
return response.ok || response.status === 400;
|
|
28173
|
+
} catch {
|
|
28174
|
+
return false;
|
|
28175
|
+
}
|
|
28176
|
+
}
|
|
28177
|
+
};
|
|
28178
|
+
}
|
|
28179
|
+
function createOpenAiProvider(apiKey, model) {
|
|
28180
|
+
const endpoint = "https://api.openai.com/v1/chat/completions";
|
|
28181
|
+
return {
|
|
28182
|
+
provider: "openai",
|
|
28183
|
+
model,
|
|
28184
|
+
async complete(system, user, maxTokens) {
|
|
28185
|
+
const response = await fetch(endpoint, {
|
|
28186
|
+
method: "POST",
|
|
28187
|
+
headers: {
|
|
28188
|
+
"Content-Type": "application/json",
|
|
28189
|
+
"Authorization": `Bearer ${apiKey}`
|
|
28190
|
+
},
|
|
28191
|
+
body: JSON.stringify({
|
|
28192
|
+
model,
|
|
28193
|
+
max_tokens: maxTokens ?? 1024,
|
|
28194
|
+
messages: [
|
|
28195
|
+
{ role: "system", content: system },
|
|
28196
|
+
{ role: "user", content: user }
|
|
28197
|
+
]
|
|
28198
|
+
})
|
|
28199
|
+
});
|
|
28200
|
+
if (!response.ok) {
|
|
28201
|
+
throw new Error(`openai api returned ${String(response.status)}`);
|
|
28202
|
+
}
|
|
28203
|
+
const body = await response.json();
|
|
28204
|
+
return extractTextContent(body);
|
|
28205
|
+
},
|
|
28206
|
+
async validate() {
|
|
28207
|
+
try {
|
|
28208
|
+
const response = await fetch(endpoint, {
|
|
28209
|
+
method: "POST",
|
|
28210
|
+
headers: {
|
|
28211
|
+
"Content-Type": "application/json",
|
|
28212
|
+
"Authorization": `Bearer ${apiKey}`
|
|
28213
|
+
},
|
|
28214
|
+
body: JSON.stringify({
|
|
28215
|
+
model,
|
|
28216
|
+
max_tokens: 1,
|
|
28217
|
+
messages: [{ role: "user", content: "hi" }]
|
|
28218
|
+
})
|
|
28219
|
+
});
|
|
28220
|
+
return response.ok;
|
|
28221
|
+
} catch {
|
|
28222
|
+
return false;
|
|
28223
|
+
}
|
|
28224
|
+
}
|
|
28225
|
+
};
|
|
28226
|
+
}
|
|
28227
|
+
function createGeminiProvider(apiKey, model) {
|
|
28228
|
+
const baseUrl = "https://generativelanguage.googleapis.com/v1beta";
|
|
28229
|
+
return {
|
|
28230
|
+
provider: "gemini",
|
|
28231
|
+
model,
|
|
28232
|
+
async complete(system, user, maxTokens) {
|
|
28233
|
+
const url = `${baseUrl}/models/${model}:generateContent?key=${apiKey}`;
|
|
28234
|
+
const response = await fetch(url, {
|
|
28235
|
+
method: "POST",
|
|
28236
|
+
headers: { "Content-Type": "application/json" },
|
|
28237
|
+
body: JSON.stringify({
|
|
28238
|
+
systemInstruction: { parts: [{ text: system }] },
|
|
28239
|
+
contents: [{ parts: [{ text: user }] }],
|
|
28240
|
+
generationConfig: { maxOutputTokens: maxTokens ?? 1024 }
|
|
28241
|
+
})
|
|
28242
|
+
});
|
|
28243
|
+
if (!response.ok) {
|
|
28244
|
+
throw new Error(`gemini api returned ${String(response.status)}`);
|
|
28245
|
+
}
|
|
28246
|
+
const body = await response.json();
|
|
28247
|
+
return extractTextContent(body);
|
|
28248
|
+
},
|
|
28249
|
+
async validate() {
|
|
28250
|
+
try {
|
|
28251
|
+
const url = `${baseUrl}/models/${model}:generateContent?key=${apiKey}`;
|
|
28252
|
+
const response = await fetch(url, {
|
|
28253
|
+
method: "POST",
|
|
28254
|
+
headers: { "Content-Type": "application/json" },
|
|
28255
|
+
body: JSON.stringify({
|
|
28256
|
+
contents: [{ parts: [{ text: "hi" }] }],
|
|
28257
|
+
generationConfig: { maxOutputTokens: 1 }
|
|
28258
|
+
})
|
|
28259
|
+
});
|
|
28260
|
+
return response.ok;
|
|
28261
|
+
} catch {
|
|
28262
|
+
return false;
|
|
28263
|
+
}
|
|
28264
|
+
}
|
|
28265
|
+
};
|
|
28266
|
+
}
|
|
28267
|
+
function createOpenRouterProvider(apiKey, model) {
|
|
28268
|
+
const endpoint = "https://openrouter.ai/api/v1/chat/completions";
|
|
28269
|
+
return {
|
|
28270
|
+
provider: "openrouter",
|
|
28271
|
+
model,
|
|
28272
|
+
async complete(system, user, maxTokens) {
|
|
28273
|
+
const response = await fetch(endpoint, {
|
|
28274
|
+
method: "POST",
|
|
28275
|
+
headers: {
|
|
28276
|
+
"Content-Type": "application/json",
|
|
28277
|
+
"Authorization": `Bearer ${apiKey}`
|
|
28278
|
+
},
|
|
28279
|
+
body: JSON.stringify({
|
|
28280
|
+
model,
|
|
28281
|
+
max_tokens: maxTokens ?? 1024,
|
|
28282
|
+
messages: [
|
|
28283
|
+
{ role: "system", content: system },
|
|
28284
|
+
{ role: "user", content: user }
|
|
28285
|
+
]
|
|
28286
|
+
})
|
|
28287
|
+
});
|
|
28288
|
+
if (!response.ok) {
|
|
28289
|
+
throw new Error(`openrouter api returned ${String(response.status)}`);
|
|
28290
|
+
}
|
|
28291
|
+
const body = await response.json();
|
|
28292
|
+
return extractTextContent(body);
|
|
28293
|
+
},
|
|
28294
|
+
async validate() {
|
|
28295
|
+
try {
|
|
28296
|
+
const response = await fetch(endpoint, {
|
|
28297
|
+
method: "POST",
|
|
28298
|
+
headers: {
|
|
28299
|
+
"Content-Type": "application/json",
|
|
28300
|
+
"Authorization": `Bearer ${apiKey}`
|
|
28301
|
+
},
|
|
28302
|
+
body: JSON.stringify({
|
|
28303
|
+
model,
|
|
28304
|
+
max_tokens: 1,
|
|
28305
|
+
messages: [{ role: "user", content: "hi" }]
|
|
28306
|
+
})
|
|
28307
|
+
});
|
|
28308
|
+
return response.ok;
|
|
28309
|
+
} catch {
|
|
28310
|
+
return false;
|
|
28311
|
+
}
|
|
28312
|
+
}
|
|
28313
|
+
};
|
|
28314
|
+
}
|
|
28315
|
+
function createLlmProvider(config) {
|
|
28316
|
+
const model = config.model ?? DEFAULT_MODELS[config.provider];
|
|
28317
|
+
switch (config.provider) {
|
|
28318
|
+
case "anthropic":
|
|
28319
|
+
return createAnthropicProvider(config.apiKey, model);
|
|
28320
|
+
case "openai":
|
|
28321
|
+
return createOpenAiProvider(config.apiKey, model);
|
|
28322
|
+
case "gemini":
|
|
28323
|
+
return createGeminiProvider(config.apiKey, model);
|
|
28324
|
+
case "openrouter":
|
|
28325
|
+
return createOpenRouterProvider(config.apiKey, model);
|
|
28326
|
+
}
|
|
28327
|
+
}
|
|
28328
|
+
|
|
28329
|
+
// packages/api/src/insights-generator.ts
|
|
28330
|
+
var SYSTEM_PROMPT = `You are an AI coding session analyst. You analyze telemetry from AI coding agent sessions and produce structured insights.
|
|
28331
|
+
|
|
28332
|
+
Return ONLY valid JSON with this exact schema:
|
|
28333
|
+
{
|
|
28334
|
+
"summary": "2-3 sentence overview of what the session accomplished",
|
|
28335
|
+
"highlights": ["1-3 notable observations about the session"],
|
|
28336
|
+
"suggestions": ["0-3 actionable suggestions for improving efficiency"],
|
|
28337
|
+
"costNote": "optional one-line note about cost efficiency, or null"
|
|
28338
|
+
}
|
|
28339
|
+
|
|
28340
|
+
Guidelines:
|
|
28341
|
+
- summary: Describe what the agent accomplished concisely. Mention key outcomes (files changed, commits, PRs).
|
|
28342
|
+
- highlights: Focus on interesting patterns \u2014 heavy tool usage, large diffs, cache efficiency, model choices.
|
|
28343
|
+
- suggestions: Only suggest things that are actionable. If the session looks efficient, return an empty array.
|
|
28344
|
+
- costNote: Comment on cost relative to output if noteworthy. Set to null if unremarkable.
|
|
28345
|
+
- Be concise and specific. Reference actual numbers from the data.`;
|
|
28346
|
+
function condensedTimeline(trace, maxChars) {
|
|
28347
|
+
const lines = [];
|
|
28348
|
+
for (const event of trace.timeline) {
|
|
28349
|
+
const parts = [event.type];
|
|
28350
|
+
if (event.details !== void 0) {
|
|
28351
|
+
const d = event.details;
|
|
28352
|
+
const toolName = d["toolName"] ?? d["tool_name"];
|
|
28353
|
+
if (typeof toolName === "string") parts.push(toolName);
|
|
28354
|
+
const toolInput = d["toolInput"] ?? d["tool_input"];
|
|
28355
|
+
if (typeof toolInput === "object" && toolInput !== null) {
|
|
28356
|
+
const ti = toolInput;
|
|
28357
|
+
const fp = ti["file_path"] ?? ti["filePath"];
|
|
28358
|
+
if (typeof fp === "string") parts.push(fp);
|
|
28359
|
+
const cmd = ti["command"];
|
|
28360
|
+
if (typeof cmd === "string") parts.push(cmd.slice(0, 80));
|
|
28361
|
+
}
|
|
28362
|
+
}
|
|
28363
|
+
if (event.costUsd !== void 0 && event.costUsd > 0) {
|
|
28364
|
+
parts.push(`$${event.costUsd.toFixed(4)}`);
|
|
28365
|
+
}
|
|
28366
|
+
const line = parts.join(" | ");
|
|
28367
|
+
lines.push(line);
|
|
28368
|
+
const totalLength = lines.reduce((sum, l) => sum + l.length + 1, 0);
|
|
28369
|
+
if (totalLength > maxChars) break;
|
|
28370
|
+
}
|
|
28371
|
+
return lines.join("\n");
|
|
28372
|
+
}
|
|
28373
|
+
function buildInsightPrompt(trace) {
|
|
28374
|
+
const m = trace.metrics;
|
|
28375
|
+
const sections = [];
|
|
28376
|
+
sections.push(`## Session: ${trace.sessionId}`);
|
|
28377
|
+
sections.push(`Started: ${trace.startedAt}${trace.endedAt !== void 0 ? ` | Ended: ${trace.endedAt}` : ""}`);
|
|
28378
|
+
if (trace.environment.gitRepo !== void 0) {
|
|
28379
|
+
sections.push(`Repo: ${trace.environment.gitRepo}${trace.environment.gitBranch !== void 0 ? ` (${trace.environment.gitBranch})` : ""}`);
|
|
28380
|
+
}
|
|
28381
|
+
sections.push(`
|
|
28382
|
+
## Metrics`);
|
|
28383
|
+
sections.push(`Prompts: ${String(m.promptCount)} | Tool calls: ${String(m.toolCallCount)} | API calls: ${String(m.apiCallCount)}`);
|
|
28384
|
+
sections.push(`Cost: $${m.totalCostUsd.toFixed(4)}`);
|
|
28385
|
+
sections.push(`Tokens: ${String(m.totalInputTokens)} in / ${String(m.totalOutputTokens)} out | Cache: ${String(m.totalCacheReadTokens)} read / ${String(m.totalCacheWriteTokens)} write`);
|
|
28386
|
+
sections.push(`Lines: +${String(m.linesAdded)} / -${String(m.linesRemoved)} | Files: ${String(m.filesTouched.length)}`);
|
|
28387
|
+
if (m.modelsUsed.length > 0) sections.push(`Models: ${m.modelsUsed.join(", ")}`);
|
|
28388
|
+
if (m.toolsUsed.length > 0) sections.push(`Tools: ${m.toolsUsed.join(", ")}`);
|
|
28389
|
+
if (trace.git.commits.length > 0) {
|
|
28390
|
+
sections.push(`
|
|
28391
|
+
## Commits (${String(trace.git.commits.length)})`);
|
|
28392
|
+
trace.git.commits.forEach((c) => {
|
|
28393
|
+
sections.push(`- ${c.sha.slice(0, 7)}: ${c.message ?? "no message"}`);
|
|
28394
|
+
});
|
|
28395
|
+
}
|
|
28396
|
+
if (trace.git.pullRequests.length > 0) {
|
|
28397
|
+
sections.push(`
|
|
28398
|
+
## Pull Requests (${String(trace.git.pullRequests.length)})`);
|
|
28399
|
+
trace.git.pullRequests.forEach((pr) => {
|
|
28400
|
+
sections.push(`- PR #${String(pr.prNumber)} (${pr.state}) in ${pr.repo}`);
|
|
28401
|
+
});
|
|
28402
|
+
}
|
|
28403
|
+
sections.push(`
|
|
28404
|
+
## Timeline (condensed)`);
|
|
28405
|
+
sections.push(condensedTimeline(trace, 4e3));
|
|
28406
|
+
return sections.join("\n");
|
|
28407
|
+
}
|
|
28408
|
+
function parseInsightJson(raw) {
|
|
28409
|
+
let jsonStr = raw.trim();
|
|
28410
|
+
const fenceMatch = jsonStr.match(/```(?:json)?\s*\n?([\s\S]*?)\n?\s*```/);
|
|
28411
|
+
if (fenceMatch !== null && fenceMatch[1] !== void 0) {
|
|
28412
|
+
jsonStr = fenceMatch[1].trim();
|
|
28413
|
+
}
|
|
28414
|
+
try {
|
|
28415
|
+
const parsed = JSON.parse(jsonStr);
|
|
28416
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return void 0;
|
|
28417
|
+
const obj = parsed;
|
|
28418
|
+
const summary = typeof obj["summary"] === "string" ? obj["summary"] : "";
|
|
28419
|
+
if (summary.length === 0) return void 0;
|
|
28420
|
+
const highlights = Array.isArray(obj["highlights"]) ? obj["highlights"].filter((h) => typeof h === "string") : [];
|
|
28421
|
+
const suggestions = Array.isArray(obj["suggestions"]) ? obj["suggestions"].filter((s) => typeof s === "string") : [];
|
|
28422
|
+
const costNote = typeof obj["costNote"] === "string" && obj["costNote"].length > 0 ? obj["costNote"] : void 0;
|
|
28423
|
+
return { summary, highlights, suggestions, costNote };
|
|
28424
|
+
} catch {
|
|
28425
|
+
return void 0;
|
|
28426
|
+
}
|
|
28427
|
+
}
|
|
28428
|
+
async function generateSessionInsight(trace, provider) {
|
|
28429
|
+
const userPrompt = buildInsightPrompt(trace);
|
|
28430
|
+
const rawResponse = await provider.complete(SYSTEM_PROMPT, userPrompt);
|
|
28431
|
+
const parsed = parseInsightJson(rawResponse);
|
|
28432
|
+
if (parsed === void 0) {
|
|
28433
|
+
return {
|
|
28434
|
+
sessionId: trace.sessionId,
|
|
28435
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
28436
|
+
provider: provider.provider,
|
|
28437
|
+
model: provider.model,
|
|
28438
|
+
summary: rawResponse.slice(0, 500) || "Failed to generate structured insight.",
|
|
28439
|
+
highlights: [],
|
|
28440
|
+
suggestions: []
|
|
28441
|
+
};
|
|
28442
|
+
}
|
|
28443
|
+
const result = {
|
|
28444
|
+
sessionId: trace.sessionId,
|
|
28445
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
28446
|
+
provider: provider.provider,
|
|
28447
|
+
model: provider.model,
|
|
28448
|
+
summary: parsed.summary,
|
|
28449
|
+
highlights: parsed.highlights,
|
|
28450
|
+
suggestions: parsed.suggestions
|
|
28451
|
+
};
|
|
28452
|
+
if (parsed.costNote !== void 0) {
|
|
28453
|
+
return { ...result, costNote: parsed.costNote };
|
|
28454
|
+
}
|
|
28455
|
+
return result;
|
|
28456
|
+
}
|
|
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
|
+
|
|
28688
|
+
// packages/api/src/insights-handler.ts
|
|
28689
|
+
var insightsCache = /* @__PURE__ */ new Map();
|
|
28690
|
+
var teamInsightCache;
|
|
28691
|
+
var VALID_PROVIDERS = ["anthropic", "openai", "gemini", "openrouter"];
|
|
28692
|
+
function isValidProvider(value) {
|
|
28693
|
+
return typeof value === "string" && VALID_PROVIDERS.includes(value);
|
|
28694
|
+
}
|
|
28695
|
+
function handleGetInsightsSettings(dependencies) {
|
|
28696
|
+
const accessor = dependencies.insightsConfigAccessor;
|
|
28697
|
+
if (accessor === void 0) {
|
|
28698
|
+
return {
|
|
28699
|
+
statusCode: 200,
|
|
28700
|
+
payload: { status: "ok", configured: false }
|
|
28701
|
+
};
|
|
28702
|
+
}
|
|
28703
|
+
const config = accessor.getConfig();
|
|
28704
|
+
if (config === void 0) {
|
|
28705
|
+
return {
|
|
28706
|
+
statusCode: 200,
|
|
28707
|
+
payload: { status: "ok", configured: false }
|
|
28708
|
+
};
|
|
28709
|
+
}
|
|
28710
|
+
return {
|
|
28711
|
+
statusCode: 200,
|
|
28712
|
+
payload: {
|
|
28713
|
+
status: "ok",
|
|
28714
|
+
configured: true,
|
|
28715
|
+
provider: config.provider,
|
|
28716
|
+
...config.model !== void 0 ? { model: config.model } : {}
|
|
28717
|
+
}
|
|
28718
|
+
};
|
|
28719
|
+
}
|
|
28720
|
+
async function handlePostInsightsSettings(body, dependencies) {
|
|
28721
|
+
const accessor = dependencies.insightsConfigAccessor;
|
|
28722
|
+
if (accessor === void 0) {
|
|
28723
|
+
return {
|
|
28724
|
+
statusCode: 500,
|
|
28725
|
+
payload: { status: "error", message: "insights settings not available" }
|
|
28726
|
+
};
|
|
28727
|
+
}
|
|
28728
|
+
if (typeof body !== "object" || body === null || Array.isArray(body)) {
|
|
28729
|
+
return {
|
|
28730
|
+
statusCode: 400,
|
|
28731
|
+
payload: { status: "error", message: "request body must be a JSON object" }
|
|
28732
|
+
};
|
|
28733
|
+
}
|
|
28734
|
+
const record = body;
|
|
28735
|
+
const provider = record["provider"];
|
|
28736
|
+
const apiKey = record["apiKey"];
|
|
28737
|
+
const model = record["model"];
|
|
28738
|
+
if (!isValidProvider(provider)) {
|
|
28739
|
+
return {
|
|
28740
|
+
statusCode: 400,
|
|
28741
|
+
payload: { status: "error", message: "invalid provider. must be one of: anthropic, openai, gemini, openrouter" }
|
|
28742
|
+
};
|
|
28743
|
+
}
|
|
28744
|
+
if (typeof apiKey !== "string" || apiKey.length === 0) {
|
|
28745
|
+
return {
|
|
28746
|
+
statusCode: 400,
|
|
28747
|
+
payload: { status: "error", message: "apiKey is required" }
|
|
28748
|
+
};
|
|
28749
|
+
}
|
|
28750
|
+
const config = {
|
|
28751
|
+
provider,
|
|
28752
|
+
apiKey,
|
|
28753
|
+
...typeof model === "string" && model.length > 0 ? { model } : {}
|
|
28754
|
+
};
|
|
28755
|
+
const llm = createLlmProvider(config);
|
|
28756
|
+
const valid = await llm.validate();
|
|
28757
|
+
if (!valid) {
|
|
28758
|
+
return {
|
|
28759
|
+
statusCode: 400,
|
|
28760
|
+
payload: { status: "error", message: "API key validation failed. Check your key and try again." }
|
|
28761
|
+
};
|
|
28762
|
+
}
|
|
28763
|
+
accessor.setConfig(config);
|
|
28764
|
+
return {
|
|
28765
|
+
statusCode: 200,
|
|
28766
|
+
payload: {
|
|
28767
|
+
status: "ok",
|
|
28768
|
+
message: "insights configuration saved",
|
|
28769
|
+
provider: config.provider,
|
|
28770
|
+
model: llm.model
|
|
28771
|
+
}
|
|
28772
|
+
};
|
|
28773
|
+
}
|
|
28774
|
+
async function handlePostSessionInsight(sessionId, dependencies) {
|
|
28775
|
+
const accessor = dependencies.insightsConfigAccessor;
|
|
28776
|
+
if (accessor === void 0) {
|
|
28777
|
+
return {
|
|
28778
|
+
statusCode: 400,
|
|
28779
|
+
payload: { status: "error", message: "insights not configured" }
|
|
28780
|
+
};
|
|
28781
|
+
}
|
|
28782
|
+
const config = accessor.getConfig();
|
|
28783
|
+
if (config === void 0) {
|
|
28784
|
+
return {
|
|
28785
|
+
statusCode: 400,
|
|
28786
|
+
payload: { status: "error", message: "no AI provider configured. open settings to add your API key." }
|
|
28787
|
+
};
|
|
28788
|
+
}
|
|
28789
|
+
const cached = insightsCache.get(sessionId);
|
|
28790
|
+
if (cached !== void 0) {
|
|
28791
|
+
return {
|
|
28792
|
+
statusCode: 200,
|
|
28793
|
+
payload: { status: "ok", insight: cached }
|
|
28794
|
+
};
|
|
28795
|
+
}
|
|
28796
|
+
const trace = dependencies.repository.getBySessionId(sessionId);
|
|
28797
|
+
if (trace === void 0) {
|
|
28798
|
+
return {
|
|
28799
|
+
statusCode: 404,
|
|
28800
|
+
payload: { status: "error", message: "session not found" }
|
|
28801
|
+
};
|
|
28802
|
+
}
|
|
28803
|
+
const provider = createLlmProvider(config);
|
|
28804
|
+
const insight = await generateSessionInsight(trace, provider);
|
|
28805
|
+
insightsCache.set(sessionId, insight);
|
|
28806
|
+
return {
|
|
28807
|
+
statusCode: 200,
|
|
28808
|
+
payload: { status: "ok", insight }
|
|
28809
|
+
};
|
|
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
|
+
};
|
|
26415
28880
|
}
|
|
26416
|
-
function
|
|
26417
|
-
|
|
26418
|
-
|
|
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
|
+
};
|
|
26419
28888
|
}
|
|
26420
|
-
|
|
26421
|
-
|
|
26422
|
-
|
|
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
|
+
};
|
|
26423
28894
|
}
|
|
26424
|
-
const
|
|
26425
|
-
const
|
|
26426
|
-
const
|
|
26427
|
-
|
|
26428
|
-
|
|
26429
|
-
|
|
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
|
+
};
|
|
26430
28914
|
}
|
|
26431
28915
|
|
|
26432
|
-
// packages/runtime/src/runtime.ts
|
|
26433
|
-
var import_node_http2 = __toESM(require("node:http"));
|
|
26434
|
-
|
|
26435
28916
|
// packages/api/src/mapper.ts
|
|
26436
28917
|
function toSessionSummary(trace) {
|
|
26437
28918
|
return {
|
|
26438
28919
|
sessionId: trace.sessionId,
|
|
26439
28920
|
userId: trace.user.id,
|
|
28921
|
+
userDisplayName: trace.user.displayName ?? null,
|
|
26440
28922
|
gitRepo: trace.environment.gitRepo ?? null,
|
|
26441
28923
|
gitBranch: trace.environment.gitBranch ?? null,
|
|
26442
28924
|
startedAt: trace.startedAt,
|
|
@@ -26450,6 +28932,353 @@ function toSessionSummary(trace) {
|
|
|
26450
28932
|
};
|
|
26451
28933
|
}
|
|
26452
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
|
+
|
|
26453
29282
|
// packages/api/src/handler.ts
|
|
26454
29283
|
function buildError(message) {
|
|
26455
29284
|
return {
|
|
@@ -26467,15 +29296,19 @@ function buildHealth(startedAtMs) {
|
|
|
26467
29296
|
function parseFilters(searchParams) {
|
|
26468
29297
|
const userId = searchParams.get("userId");
|
|
26469
29298
|
const repo = searchParams.get("repo");
|
|
29299
|
+
const from = searchParams.get("from");
|
|
29300
|
+
const to = searchParams.get("to");
|
|
26470
29301
|
return {
|
|
26471
29302
|
...userId !== null ? { userId } : {},
|
|
26472
|
-
...repo !== null ? { repo } : {}
|
|
29303
|
+
...repo !== null ? { repo } : {},
|
|
29304
|
+
...from !== null && from.length === 10 ? { from } : {},
|
|
29305
|
+
...to !== null && to.length === 10 ? { to } : {}
|
|
26473
29306
|
};
|
|
26474
29307
|
}
|
|
26475
29308
|
function parseSessionPath(pathname) {
|
|
26476
29309
|
return pathname.split("/").filter((segment) => segment.length > 0);
|
|
26477
29310
|
}
|
|
26478
|
-
function
|
|
29311
|
+
function toMetricDate2(startedAt) {
|
|
26479
29312
|
const parsed = Date.parse(startedAt);
|
|
26480
29313
|
if (Number.isNaN(parsed)) {
|
|
26481
29314
|
return startedAt.slice(0, 10);
|
|
@@ -26486,7 +29319,7 @@ function buildDailyCostResponseFromTraces(dependencies, filters) {
|
|
|
26486
29319
|
const traces = dependencies.repository.list(filters);
|
|
26487
29320
|
const byDate = /* @__PURE__ */ new Map();
|
|
26488
29321
|
traces.forEach((trace) => {
|
|
26489
|
-
const date =
|
|
29322
|
+
const date = toMetricDate2(trace.startedAt);
|
|
26490
29323
|
const current = byDate.get(date) ?? {
|
|
26491
29324
|
totalCostUsd: 0,
|
|
26492
29325
|
sessionCount: 0,
|
|
@@ -26550,6 +29383,43 @@ async function handleApiRequest(request, dependencies) {
|
|
|
26550
29383
|
payload: await buildDailyCostResponse(dependencies, filters)
|
|
26551
29384
|
};
|
|
26552
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
|
+
}
|
|
29413
|
+
if (request.method === "GET" && pathname === "/v1/settings/insights") {
|
|
29414
|
+
return handleGetInsightsSettings(dependencies);
|
|
29415
|
+
}
|
|
29416
|
+
if (request.method === "POST" && pathname === "/v1/settings/insights") {
|
|
29417
|
+
return handlePostInsightsSettings(request.body, dependencies);
|
|
29418
|
+
}
|
|
29419
|
+
const insightsMatch = pathname.match(/^\/v1\/sessions\/([^/]+)\/insights$/);
|
|
29420
|
+
if (request.method === "POST" && insightsMatch !== null && insightsMatch[1] !== void 0) {
|
|
29421
|
+
return handlePostSessionInsight(decodeURIComponent(insightsMatch[1]), dependencies);
|
|
29422
|
+
}
|
|
26553
29423
|
if (request.method === "GET") {
|
|
26554
29424
|
const segments = parseSessionPath(pathname);
|
|
26555
29425
|
if (segments.length >= 3 && segments[0] === "v1" && segments[1] === "sessions") {
|
|
@@ -26597,11 +29467,21 @@ async function handleApiRequest(request, dependencies) {
|
|
|
26597
29467
|
|
|
26598
29468
|
// packages/api/src/http.ts
|
|
26599
29469
|
function normalizeMethod(method) {
|
|
26600
|
-
if (method === "GET") {
|
|
29470
|
+
if (method === "GET" || method === "POST") {
|
|
26601
29471
|
return method;
|
|
26602
29472
|
}
|
|
26603
29473
|
return void 0;
|
|
26604
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
|
+
}
|
|
26605
29485
|
function sendJson2(res, statusCode, payload) {
|
|
26606
29486
|
const body = JSON.stringify(payload);
|
|
26607
29487
|
res.statusCode = statusCode;
|
|
@@ -26664,30 +29544,76 @@ async function handleApiRawHttpRequest(request, dependencies) {
|
|
|
26664
29544
|
return handleApiRequest(
|
|
26665
29545
|
{
|
|
26666
29546
|
method,
|
|
26667
|
-
url: request.url
|
|
29547
|
+
url: request.url,
|
|
29548
|
+
...request.body !== void 0 ? { body: request.body } : {}
|
|
26668
29549
|
},
|
|
26669
29550
|
dependencies
|
|
26670
29551
|
);
|
|
26671
29552
|
}
|
|
29553
|
+
function readRequestBody(req) {
|
|
29554
|
+
return new Promise((resolve, reject) => {
|
|
29555
|
+
let data = "";
|
|
29556
|
+
req.setEncoding("utf8");
|
|
29557
|
+
req.on("data", (chunk) => {
|
|
29558
|
+
data += chunk;
|
|
29559
|
+
});
|
|
29560
|
+
req.on("end", () => {
|
|
29561
|
+
if (data.length === 0) {
|
|
29562
|
+
resolve(void 0);
|
|
29563
|
+
return;
|
|
29564
|
+
}
|
|
29565
|
+
try {
|
|
29566
|
+
resolve(JSON.parse(data));
|
|
29567
|
+
} catch {
|
|
29568
|
+
reject(new Error("invalid JSON body"));
|
|
29569
|
+
}
|
|
29570
|
+
});
|
|
29571
|
+
req.on("error", (error) => reject(error));
|
|
29572
|
+
});
|
|
29573
|
+
}
|
|
26672
29574
|
function createApiHttpHandler(dependencies) {
|
|
26673
29575
|
return (req, res) => {
|
|
26674
29576
|
const method = req.method ?? "GET";
|
|
26675
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
|
+
}
|
|
26676
29590
|
if (isSseSessionsRoute(method, url)) {
|
|
26677
29591
|
startSessionsSseStream(req, res, dependencies);
|
|
26678
29592
|
return;
|
|
26679
29593
|
}
|
|
26680
|
-
|
|
26681
|
-
|
|
26682
|
-
|
|
26683
|
-
|
|
26684
|
-
|
|
26685
|
-
|
|
26686
|
-
|
|
26687
|
-
|
|
26688
|
-
|
|
26689
|
-
|
|
26690
|
-
|
|
29594
|
+
const dispatch = (body) => {
|
|
29595
|
+
void handleApiRawHttpRequest(
|
|
29596
|
+
{
|
|
29597
|
+
method,
|
|
29598
|
+
url,
|
|
29599
|
+
...body !== void 0 ? { body } : {}
|
|
29600
|
+
},
|
|
29601
|
+
dependencies
|
|
29602
|
+
).then((response) => {
|
|
29603
|
+
sendJson2(res, response.statusCode, response.payload);
|
|
29604
|
+
}).catch(() => {
|
|
29605
|
+
sendJson2(res, 500, { status: "error", message: "internal server error" });
|
|
29606
|
+
});
|
|
29607
|
+
};
|
|
29608
|
+
if (method === "POST") {
|
|
29609
|
+
readRequestBody(req).then((body) => {
|
|
29610
|
+
dispatch(body);
|
|
29611
|
+
}).catch(() => {
|
|
29612
|
+
sendJson2(res, 400, { status: "error", message: "invalid request body" });
|
|
29613
|
+
});
|
|
29614
|
+
return;
|
|
29615
|
+
}
|
|
29616
|
+
dispatch();
|
|
26691
29617
|
};
|
|
26692
29618
|
}
|
|
26693
29619
|
|
|
@@ -26709,6 +29635,18 @@ var InMemorySessionRepository = class {
|
|
|
26709
29635
|
if (filters.repo !== void 0 && trace.environment.gitRepo !== filters.repo) {
|
|
26710
29636
|
return false;
|
|
26711
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
|
+
}
|
|
26712
29650
|
return true;
|
|
26713
29651
|
});
|
|
26714
29652
|
}
|
|
@@ -26962,6 +29900,26 @@ function parsePathname3(url) {
|
|
|
26962
29900
|
return url;
|
|
26963
29901
|
}
|
|
26964
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
|
+
}
|
|
26965
29923
|
function handleCollectorRawHttpRequest(request, dependencies) {
|
|
26966
29924
|
const method = normalizeMethod2(request.method);
|
|
26967
29925
|
if (method === void 0) {
|
|
@@ -26973,6 +29931,12 @@ function handleCollectorRawHttpRequest(request, dependencies) {
|
|
|
26973
29931
|
}
|
|
26974
29932
|
};
|
|
26975
29933
|
}
|
|
29934
|
+
if (method === "POST") {
|
|
29935
|
+
const authError = checkTeamAuth(request.authorizationHeader);
|
|
29936
|
+
if (authError !== void 0) {
|
|
29937
|
+
return authError;
|
|
29938
|
+
}
|
|
29939
|
+
}
|
|
26976
29940
|
const pathname = parsePathname3(request.url);
|
|
26977
29941
|
if (method === "POST" && pathname === "/v1/hooks") {
|
|
26978
29942
|
const rawBody = request.rawBody ?? "";
|
|
@@ -27014,7 +29978,7 @@ function handleCollectorRawHttpRequest(request, dependencies) {
|
|
|
27014
29978
|
dependencies
|
|
27015
29979
|
);
|
|
27016
29980
|
}
|
|
27017
|
-
async function
|
|
29981
|
+
async function readRequestBody2(req) {
|
|
27018
29982
|
return new Promise((resolve, reject) => {
|
|
27019
29983
|
const chunks = [];
|
|
27020
29984
|
req.on("data", (chunk) => chunks.push(chunk));
|
|
@@ -27032,12 +29996,14 @@ function createCollectorHttpHandler(dependencies) {
|
|
|
27032
29996
|
return async (req, res) => {
|
|
27033
29997
|
const method = req.method ?? "GET";
|
|
27034
29998
|
const url = req.url ?? "/";
|
|
27035
|
-
const rawBody = method === "POST" ? await
|
|
29999
|
+
const rawBody = method === "POST" ? await readRequestBody2(req) : void 0;
|
|
30000
|
+
const authorizationHeader = typeof req.headers["authorization"] === "string" ? req.headers["authorization"] : void 0;
|
|
27036
30001
|
const response = handleCollectorRawHttpRequest(
|
|
27037
30002
|
{
|
|
27038
30003
|
method,
|
|
27039
30004
|
url,
|
|
27040
|
-
...rawBody !== void 0 ? { rawBody } : {}
|
|
30005
|
+
...rawBody !== void 0 ? { rawBody } : {},
|
|
30006
|
+
...authorizationHeader !== void 0 ? { authorizationHeader } : {}
|
|
27041
30007
|
},
|
|
27042
30008
|
dependencies
|
|
27043
30009
|
);
|
|
@@ -28526,12 +31492,16 @@ function toBaseTrace(envelope) {
|
|
|
28526
31492
|
const gitRepo = readString4(payload, ["git_repo", "gitRepo"]);
|
|
28527
31493
|
const gitBranch = readString4(payload, ["git_branch", "gitBranch"]);
|
|
28528
31494
|
const projectPath = readString4(payload, ["project_path", "projectPath"]);
|
|
28529
|
-
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";
|
|
28530
31498
|
return {
|
|
28531
31499
|
sessionId: envelope.sessionId,
|
|
28532
31500
|
agentType: "claude_code",
|
|
28533
31501
|
user: {
|
|
28534
|
-
id: userId
|
|
31502
|
+
id: userId,
|
|
31503
|
+
...userEmail !== void 0 ? { email: userEmail } : {},
|
|
31504
|
+
...userName !== void 0 ? { displayName: userName } : {}
|
|
28535
31505
|
},
|
|
28536
31506
|
environment: {
|
|
28537
31507
|
...projectPath !== void 0 ? { projectPath } : {},
|
|
@@ -28588,6 +31558,13 @@ function toUpdatedTrace(existing, envelope) {
|
|
|
28588
31558
|
const mergedTimeline = [...existing.timeline, timelineEvent];
|
|
28589
31559
|
const endedAt = shouldMarkEnded(envelope.eventType) ? envelope.eventTimestamp : existing.endedAt;
|
|
28590
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
|
+
};
|
|
28591
31568
|
const cost = computeEventCost(payload) ?? 0;
|
|
28592
31569
|
const inputTokens = readNumber3(payload, ["input_tokens", "inputTokens"]) ?? 0;
|
|
28593
31570
|
const outputTokens = readNumber3(payload, ["output_tokens", "outputTokens"]) ?? 0;
|
|
@@ -28656,6 +31633,7 @@ function toUpdatedTrace(existing, envelope) {
|
|
|
28656
31633
|
}
|
|
28657
31634
|
return {
|
|
28658
31635
|
...existing,
|
|
31636
|
+
user: updatedUser,
|
|
28659
31637
|
...endedAt !== void 0 ? { endedAt } : {},
|
|
28660
31638
|
activeDurationMs: updateDurationMs(existing.startedAt, latestTime),
|
|
28661
31639
|
timeline: mergedTimeline,
|
|
@@ -28740,7 +31718,9 @@ function resolveRuntimeOptions(input) {
|
|
|
28740
31718
|
return {
|
|
28741
31719
|
startedAtMs: input,
|
|
28742
31720
|
persistence: new InMemoryRuntimePersistence(),
|
|
28743
|
-
dailyCostReader: void 0
|
|
31721
|
+
dailyCostReader: void 0,
|
|
31722
|
+
insightsConfigAccessor: void 0,
|
|
31723
|
+
teamBudgetStore: void 0
|
|
28744
31724
|
};
|
|
28745
31725
|
}
|
|
28746
31726
|
const startedAtMs = input?.startedAtMs ?? Date.now();
|
|
@@ -28748,7 +31728,9 @@ function resolveRuntimeOptions(input) {
|
|
|
28748
31728
|
return {
|
|
28749
31729
|
startedAtMs,
|
|
28750
31730
|
persistence,
|
|
28751
|
-
dailyCostReader: input?.dailyCostReader
|
|
31731
|
+
dailyCostReader: input?.dailyCostReader,
|
|
31732
|
+
insightsConfigAccessor: input?.insightsConfigAccessor,
|
|
31733
|
+
teamBudgetStore: input?.teamBudgetStore
|
|
28752
31734
|
};
|
|
28753
31735
|
}
|
|
28754
31736
|
function createInMemoryRuntime(input) {
|
|
@@ -28766,7 +31748,9 @@ function createInMemoryRuntime(input) {
|
|
|
28766
31748
|
const apiDependencies = {
|
|
28767
31749
|
startedAtMs: options.startedAtMs,
|
|
28768
31750
|
repository: sessionRepository,
|
|
28769
|
-
...options.dailyCostReader !== void 0 ? { dailyCostReader: options.dailyCostReader } : {}
|
|
31751
|
+
...options.dailyCostReader !== void 0 ? { dailyCostReader: options.dailyCostReader } : {},
|
|
31752
|
+
...options.insightsConfigAccessor !== void 0 ? { insightsConfigAccessor: options.insightsConfigAccessor } : {},
|
|
31753
|
+
...options.teamBudgetStore !== void 0 ? { teamBudgetStore: options.teamBudgetStore } : {}
|
|
28770
31754
|
};
|
|
28771
31755
|
return {
|
|
28772
31756
|
sessionRepository,
|
|
@@ -28775,6 +31759,8 @@ function createInMemoryRuntime(input) {
|
|
|
28775
31759
|
collectorService,
|
|
28776
31760
|
persistence,
|
|
28777
31761
|
...options.dailyCostReader !== void 0 ? { dailyCostReader: options.dailyCostReader } : {},
|
|
31762
|
+
...options.insightsConfigAccessor !== void 0 ? { insightsConfigAccessor: options.insightsConfigAccessor } : {},
|
|
31763
|
+
...options.teamBudgetStore !== void 0 ? { teamBudgetStore: options.teamBudgetStore } : {},
|
|
28778
31764
|
handleCollectorRaw: collectorService.handleRaw,
|
|
28779
31765
|
handleApiRaw: (request) => handleApiRawHttpRequest(request, apiDependencies)
|
|
28780
31766
|
};
|
|
@@ -28795,7 +31781,9 @@ async function startInMemoryRuntimeServers(runtime, options = {}) {
|
|
|
28795
31781
|
createApiHttpHandler({
|
|
28796
31782
|
startedAtMs: runtime.collectorDependencies.startedAtMs,
|
|
28797
31783
|
repository: runtime.sessionRepository,
|
|
28798
|
-
...runtime.dailyCostReader !== void 0 ? { dailyCostReader: runtime.dailyCostReader } : {}
|
|
31784
|
+
...runtime.dailyCostReader !== void 0 ? { dailyCostReader: runtime.dailyCostReader } : {},
|
|
31785
|
+
...runtime.insightsConfigAccessor !== void 0 ? { insightsConfigAccessor: runtime.insightsConfigAccessor } : {},
|
|
31786
|
+
...runtime.teamBudgetStore !== void 0 ? { teamBudgetStore: runtime.teamBudgetStore } : {}
|
|
28799
31787
|
})
|
|
28800
31788
|
) : void 0;
|
|
28801
31789
|
let otelReceiver;
|
|
@@ -28964,14 +31952,86 @@ function hydrateFromSqlite(runtime, sqlite, limit, eventLimit) {
|
|
|
28964
31952
|
}
|
|
28965
31953
|
return traces.length;
|
|
28966
31954
|
}
|
|
31955
|
+
var VALID_INSIGHTS_PROVIDERS = ["anthropic", "openai", "gemini", "openrouter"];
|
|
31956
|
+
function createSqliteInsightsConfigAccessor(sqlite) {
|
|
31957
|
+
let cached;
|
|
31958
|
+
let cachedContext;
|
|
31959
|
+
const raw = sqlite.getSetting("insights_config");
|
|
31960
|
+
if (raw !== void 0) {
|
|
31961
|
+
try {
|
|
31962
|
+
const parsed = JSON.parse(raw);
|
|
31963
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
31964
|
+
const obj = parsed;
|
|
31965
|
+
if (typeof obj["provider"] === "string" && VALID_INSIGHTS_PROVIDERS.includes(obj["provider"]) && typeof obj["apiKey"] === "string") {
|
|
31966
|
+
cached = {
|
|
31967
|
+
provider: obj["provider"],
|
|
31968
|
+
apiKey: obj["apiKey"],
|
|
31969
|
+
...typeof obj["model"] === "string" && obj["model"].length > 0 ? { model: obj["model"] } : {}
|
|
31970
|
+
};
|
|
31971
|
+
}
|
|
31972
|
+
}
|
|
31973
|
+
} catch {
|
|
31974
|
+
}
|
|
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
|
+
}
|
|
31993
|
+
return {
|
|
31994
|
+
getConfig() {
|
|
31995
|
+
return cached;
|
|
31996
|
+
},
|
|
31997
|
+
setConfig(config) {
|
|
31998
|
+
cached = config;
|
|
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);
|
|
32020
|
+
}
|
|
32021
|
+
};
|
|
32022
|
+
}
|
|
28967
32023
|
function createSqliteBackedRuntime(options) {
|
|
28968
32024
|
const sqlite = new SqliteClient(options.dbPath);
|
|
28969
32025
|
const persistence = new SqlitePersistence(sqlite);
|
|
28970
32026
|
const dailyCostReader = new SqliteDailyCostReader(sqlite);
|
|
32027
|
+
const insightsConfigAccessor = options.insightsConfigAccessor ?? createSqliteInsightsConfigAccessor(sqlite);
|
|
32028
|
+
const teamBudgetStore = createSqliteTeamBudgetStore(sqlite);
|
|
28971
32029
|
const runtime = createInMemoryRuntime({
|
|
28972
32030
|
...options.startedAtMs !== void 0 ? { startedAtMs: options.startedAtMs } : {},
|
|
28973
32031
|
persistence,
|
|
28974
|
-
dailyCostReader
|
|
32032
|
+
dailyCostReader,
|
|
32033
|
+
insightsConfigAccessor,
|
|
32034
|
+
teamBudgetStore
|
|
28975
32035
|
});
|
|
28976
32036
|
const hydratedCount = hydrateFromSqlite(runtime, sqlite, options.bootstrapLimit, options.eventLimit);
|
|
28977
32037
|
const syncIntervalMs = options.syncIntervalMs ?? 5e3;
|
|
@@ -29017,6 +32077,9 @@ function parseArgs(argv) {
|
|
|
29017
32077
|
let privacyTier;
|
|
29018
32078
|
let installHooks;
|
|
29019
32079
|
let forward = false;
|
|
32080
|
+
let teamUrl;
|
|
32081
|
+
let teamPrivacyTier;
|
|
32082
|
+
let teamToken;
|
|
29020
32083
|
for (let i = 3; i < argv.length; i += 1) {
|
|
29021
32084
|
const token = argv[i];
|
|
29022
32085
|
if (token === "--forward") {
|
|
@@ -29053,6 +32116,28 @@ function parseArgs(argv) {
|
|
|
29053
32116
|
i += 1;
|
|
29054
32117
|
continue;
|
|
29055
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
|
+
}
|
|
29056
32141
|
}
|
|
29057
32142
|
return {
|
|
29058
32143
|
command,
|
|
@@ -29060,10 +32145,16 @@ function parseArgs(argv) {
|
|
|
29060
32145
|
...collectorUrl !== void 0 ? { collectorUrl } : {},
|
|
29061
32146
|
...privacyTier !== void 0 ? { privacyTier } : {},
|
|
29062
32147
|
...installHooks !== void 0 ? { installHooks } : {},
|
|
29063
|
-
...forward ? { forward: true } : {}
|
|
32148
|
+
...forward ? { forward: true } : {},
|
|
32149
|
+
...teamUrl !== void 0 ? { teamUrl } : {},
|
|
32150
|
+
...teamPrivacyTier !== void 0 ? { teamPrivacyTier } : {},
|
|
32151
|
+
...teamToken !== void 0 ? { teamToken } : {}
|
|
29064
32152
|
};
|
|
29065
32153
|
}
|
|
29066
32154
|
|
|
32155
|
+
// packages/cli/src/init.ts
|
|
32156
|
+
var import_node_child_process = require("node:child_process");
|
|
32157
|
+
|
|
29067
32158
|
// packages/cli/src/claude-hooks.ts
|
|
29068
32159
|
var HOOK_EVENTS = [
|
|
29069
32160
|
"SessionStart",
|
|
@@ -29115,12 +32206,22 @@ function parseConfig(raw) {
|
|
|
29115
32206
|
if (typeof parsed["updatedAt"] !== "string" || parsed["updatedAt"].length === 0) {
|
|
29116
32207
|
return void 0;
|
|
29117
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;
|
|
29118
32214
|
return {
|
|
29119
32215
|
version: "1.0",
|
|
29120
32216
|
collectorUrl: parsed["collectorUrl"],
|
|
29121
32217
|
privacyTier: parsed["privacyTier"],
|
|
29122
32218
|
hookCommand: parsed["hookCommand"],
|
|
29123
|
-
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 } : {}
|
|
29124
32225
|
};
|
|
29125
32226
|
}
|
|
29126
32227
|
function isRecord2(value) {
|
|
@@ -29348,6 +32449,17 @@ function deriveOtelLogsEndpoint(collectorUrl) {
|
|
|
29348
32449
|
return "http://127.0.0.1:4717";
|
|
29349
32450
|
}
|
|
29350
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
|
+
}
|
|
29351
32463
|
function buildTelemetryEnv(collectorUrl, privacyTier) {
|
|
29352
32464
|
const enablePromptAndToolDetail = privacyTier >= 2;
|
|
29353
32465
|
const otelLogsEndpoint = deriveOtelLogsEndpoint(collectorUrl);
|
|
@@ -29364,12 +32476,19 @@ function buildTelemetryEnv(collectorUrl, privacyTier) {
|
|
|
29364
32476
|
}
|
|
29365
32477
|
function runInit(input, store = new FileCliConfigStore()) {
|
|
29366
32478
|
const timestamp = nowIso(input.nowIso);
|
|
32479
|
+
const userEmail = readGitConfig("user.email");
|
|
32480
|
+
const userName = readGitConfig("user.name");
|
|
29367
32481
|
const config = {
|
|
29368
32482
|
version: "1.0",
|
|
29369
32483
|
collectorUrl: input.collectorUrl ?? "http://127.0.0.1:8317/v1/hooks",
|
|
29370
32484
|
privacyTier: ensurePrivacyTierOrDefault(input.privacyTier),
|
|
29371
32485
|
hookCommand: "agent-trace hook-handler --forward",
|
|
29372
|
-
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 } : {}
|
|
29373
32492
|
};
|
|
29374
32493
|
const telemetryEnv = buildTelemetryEnv(config.collectorUrl, config.privacyTier);
|
|
29375
32494
|
const hooks = buildClaudeHookConfig(config.hookCommand, timestamp);
|
|
@@ -29419,7 +32538,7 @@ function runStatus(configDir, store = new FileCliConfigStore()) {
|
|
|
29419
32538
|
|
|
29420
32539
|
// packages/cli/src/hook-handler.ts
|
|
29421
32540
|
var import_node_crypto4 = __toESM(require("node:crypto"));
|
|
29422
|
-
var
|
|
32541
|
+
var import_node_child_process2 = require("node:child_process");
|
|
29423
32542
|
var import_node_fs4 = __toESM(require("node:fs"));
|
|
29424
32543
|
var import_node_path4 = __toESM(require("node:path"));
|
|
29425
32544
|
function isIsoDate2(value) {
|
|
@@ -29852,7 +32971,7 @@ var FileHookSessionBaselineStore = class {
|
|
|
29852
32971
|
};
|
|
29853
32972
|
function runGitCommand(args, repositoryPath) {
|
|
29854
32973
|
try {
|
|
29855
|
-
const output = (0,
|
|
32974
|
+
const output = (0, import_node_child_process2.execFileSync)("git", [...args], {
|
|
29856
32975
|
...repositoryPath !== void 0 ? { cwd: repositoryPath } : {},
|
|
29857
32976
|
encoding: "utf8",
|
|
29858
32977
|
stdio: ["ignore", "pipe", "ignore"]
|
|
@@ -30080,9 +33199,23 @@ function runHookHandler(input, store = new FileCliConfigStore(), gitContextProvi
|
|
|
30080
33199
|
}
|
|
30081
33200
|
const now = input.nowIso ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
30082
33201
|
const privacyTier = getPrivacyTier(store, input.configDir);
|
|
33202
|
+
const config = store.readConfig(input.configDir);
|
|
30083
33203
|
const baselineStore = new FileHookSessionBaselineStore(store, input.configDir);
|
|
30084
33204
|
const enrichment = enrichHookPayloadWithGitContext(payload, gitContextProvider, baselineStore, now);
|
|
30085
|
-
|
|
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, {
|
|
30086
33219
|
...enrichment.enriched ? { git_enriched: "1" } : {},
|
|
30087
33220
|
...enrichment.usedSessionBaselineDelta ? { git_session_delta: "1" } : {}
|
|
30088
33221
|
});
|
|
@@ -30098,14 +33231,64 @@ function runHookHandler(input, store = new FileCliConfigStore(), gitContextProvi
|
|
|
30098
33231
|
envelope
|
|
30099
33232
|
};
|
|
30100
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
|
+
}
|
|
30101
33282
|
var FetchCollectorHttpClient = class {
|
|
30102
|
-
async postJson(url, payload) {
|
|
33283
|
+
async postJson(url, payload, extraHeaders) {
|
|
30103
33284
|
try {
|
|
33285
|
+
const headers = {
|
|
33286
|
+
"Content-Type": "application/json",
|
|
33287
|
+
...extraHeaders
|
|
33288
|
+
};
|
|
30104
33289
|
const response = await fetch(url, {
|
|
30105
33290
|
method: "POST",
|
|
30106
|
-
headers
|
|
30107
|
-
"Content-Type": "application/json"
|
|
30108
|
-
},
|
|
33291
|
+
headers,
|
|
30109
33292
|
body: JSON.stringify(payload)
|
|
30110
33293
|
});
|
|
30111
33294
|
const body = await response.text();
|
|
@@ -30127,6 +33310,17 @@ var FetchCollectorHttpClient = class {
|
|
|
30127
33310
|
function isSuccessStatus(statusCode) {
|
|
30128
33311
|
return statusCode >= 200 && statusCode < 300;
|
|
30129
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
|
+
}
|
|
30130
33324
|
async function runHookHandlerAndForward(input, client = new FetchCollectorHttpClient(), store = new FileCliConfigStore(), gitContextProvider = new ShellHookGitContextProvider()) {
|
|
30131
33325
|
const hookResult = runHookHandler(
|
|
30132
33326
|
{
|
|
@@ -30144,7 +33338,36 @@ async function runHookHandlerAndForward(input, client = new FetchCollectorHttpCl
|
|
|
30144
33338
|
};
|
|
30145
33339
|
}
|
|
30146
33340
|
const collectorUrl = getCollectorUrl(store, input.configDir, input.collectorUrl);
|
|
30147
|
-
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
|
+
}
|
|
30148
33371
|
if (!postResult.ok) {
|
|
30149
33372
|
return {
|
|
30150
33373
|
ok: false,
|
|
@@ -30168,7 +33391,8 @@ async function runHookHandlerAndForward(input, client = new FetchCollectorHttpCl
|
|
|
30168
33391
|
envelope: hookResult.envelope,
|
|
30169
33392
|
collectorUrl,
|
|
30170
33393
|
statusCode: postResult.statusCode,
|
|
30171
|
-
body: postResult.body
|
|
33394
|
+
body: postResult.body,
|
|
33395
|
+
...teamForwardError !== void 0 ? { teamForwardError } : {}
|
|
30172
33396
|
};
|
|
30173
33397
|
}
|
|
30174
33398
|
|
|
@@ -30293,13 +33517,17 @@ async function startServer() {
|
|
|
30293
33517
|
throw error;
|
|
30294
33518
|
}
|
|
30295
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;
|
|
30296
33523
|
let dashboardAddress;
|
|
30297
33524
|
try {
|
|
30298
33525
|
const dashboard = await startDashboardServer({
|
|
30299
33526
|
host,
|
|
30300
33527
|
port: dashboardPort,
|
|
30301
33528
|
apiBaseUrl,
|
|
30302
|
-
startedAtMs
|
|
33529
|
+
startedAtMs,
|
|
33530
|
+
...currentUserEmail !== void 0 ? { currentUserEmail } : {}
|
|
30303
33531
|
});
|
|
30304
33532
|
dashboardAddress = dashboard.address;
|
|
30305
33533
|
const originalClose = servers.close;
|