@cryptiklemur/lattice 1.5.0 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,101 @@
1
+ import {
2
+ ScatterChart,
3
+ Scatter,
4
+ XAxis,
5
+ YAxis,
6
+ CartesianGrid,
7
+ Tooltip,
8
+ ResponsiveContainer,
9
+ } from "recharts";
10
+
11
+ var TICK_STYLE = {
12
+ fontSize: 10,
13
+ fontFamily: "var(--font-mono)",
14
+ fill: "oklch(0.9 0.02 280 / 0.3)",
15
+ };
16
+
17
+ var GRID_COLOR = "oklch(0.9 0.02 280 / 0.06)";
18
+
19
+ var MODEL_COLORS: Record<string, string> = {
20
+ opus: "#a855f7",
21
+ sonnet: "oklch(55% 0.25 280)",
22
+ haiku: "#22c55e",
23
+ other: "#f59e0b",
24
+ };
25
+
26
+ interface ResponseTimeDatum {
27
+ tokens: number;
28
+ duration: number;
29
+ model: string;
30
+ sessionId: string;
31
+ }
32
+
33
+ interface ResponseTimeScatterProps {
34
+ data: ResponseTimeDatum[];
35
+ }
36
+
37
+ function CustomTooltip({ active, payload }: { active?: boolean; payload?: Array<{ payload: { tokens: number; durationSec: number; model: string } }> }) {
38
+ if (!active || !payload || payload.length === 0) return null;
39
+ var d = payload[0].payload;
40
+ return (
41
+ <div className="rounded-lg border border-base-content/8 bg-base-200 px-3 py-2 shadow-lg">
42
+ <div className="text-[11px] font-mono text-base-content/70 space-y-0.5">
43
+ <p><span className="text-base-content/40">tokens </span>{d.tokens.toLocaleString()}</p>
44
+ <p><span className="text-base-content/40">duration </span>{d.durationSec.toFixed(1)}s</p>
45
+ <p><span className="text-base-content/40">model </span>{d.model}</p>
46
+ </div>
47
+ </div>
48
+ );
49
+ }
50
+
51
+ export function ResponseTimeScatter({ data }: ResponseTimeScatterProps) {
52
+ var models = Array.from(new Set(data.map(function (d) { return d.model; })));
53
+
54
+ var byModel = models.map(function (model) {
55
+ return {
56
+ model: model,
57
+ color: MODEL_COLORS[model] || "#f59e0b",
58
+ points: data
59
+ .filter(function (d) { return d.model === model; })
60
+ .map(function (d) { return { tokens: d.tokens, durationSec: d.duration / 1000, model: d.model }; }),
61
+ };
62
+ });
63
+
64
+ return (
65
+ <ResponsiveContainer width="100%" height={200}>
66
+ <ScatterChart margin={{ top: 4, right: 4, left: -20, bottom: 0 }}>
67
+ <CartesianGrid strokeDasharray="3 3" stroke={GRID_COLOR} />
68
+ <XAxis
69
+ dataKey="tokens"
70
+ type="number"
71
+ tick={TICK_STYLE}
72
+ axisLine={false}
73
+ tickLine={false}
74
+ tickFormatter={function (v) { return v >= 1000 ? (v / 1000).toFixed(0) + "k" : String(v); }}
75
+ name="tokens"
76
+ />
77
+ <YAxis
78
+ dataKey="durationSec"
79
+ type="number"
80
+ tick={TICK_STYLE}
81
+ axisLine={false}
82
+ tickLine={false}
83
+ tickFormatter={function (v) { return v.toFixed(0) + "s"; }}
84
+ name="duration"
85
+ />
86
+ <Tooltip content={<CustomTooltip />} cursor={{ strokeDasharray: "3 3", stroke: GRID_COLOR }} />
87
+ {byModel.map(function (group) {
88
+ return (
89
+ <Scatter
90
+ key={group.model}
91
+ name={group.model}
92
+ data={group.points}
93
+ fill={group.color}
94
+ fillOpacity={0.7}
95
+ />
96
+ );
97
+ })}
98
+ </ScatterChart>
99
+ </ResponsiveContainer>
100
+ );
101
+ }
@@ -0,0 +1,112 @@
1
+ import { useState } from "react";
2
+
3
+ interface TimelineDatum {
4
+ id: string;
5
+ title: string;
6
+ project: string;
7
+ start: number;
8
+ end: number;
9
+ cost: number;
10
+ }
11
+
12
+ interface SessionTimelineProps {
13
+ data: TimelineDatum[];
14
+ }
15
+
16
+ var ROW_HEIGHT = 16;
17
+ var ROW_GAP = 2;
18
+ var ROW_STEP = ROW_HEIGHT + ROW_GAP;
19
+ var LEFT_MARGIN = 8;
20
+ var RIGHT_MARGIN = 8;
21
+
22
+ var PROJECT_PALETTE = [
23
+ "oklch(55% 0.25 280)",
24
+ "#a855f7",
25
+ "#22c55e",
26
+ "#f59e0b",
27
+ "oklch(65% 0.2 240)",
28
+ "oklch(65% 0.25 25)",
29
+ "oklch(65% 0.25 150)",
30
+ "oklch(70% 0.2 60)",
31
+ ];
32
+
33
+ function formatTime(ts: number): string {
34
+ var d = new Date(ts);
35
+ return (d.getMonth() + 1) + "/" + d.getDate() + " " + String(d.getHours()).padStart(2, "0") + ":" + String(d.getMinutes()).padStart(2, "0");
36
+ }
37
+
38
+ export function SessionTimeline({ data }: SessionTimelineProps) {
39
+ var [hover, setHover] = useState<{ x: number; y: number; datum: TimelineDatum } | null>(null);
40
+
41
+ if (!data || data.length === 0) {
42
+ return (
43
+ <div className="flex items-center justify-center h-[200px] text-base-content/25 font-mono text-[11px]">
44
+ No data
45
+ </div>
46
+ );
47
+ }
48
+
49
+ var projects = Array.from(new Set(data.map(function (d) { return d.project; })));
50
+ function getColor(project: string): string {
51
+ var idx = projects.indexOf(project);
52
+ return PROJECT_PALETTE[idx % PROJECT_PALETTE.length];
53
+ }
54
+
55
+ var minTime = Infinity;
56
+ var maxTime = -Infinity;
57
+ for (var i = 0; i < data.length; i++) {
58
+ if (data[i].start < minTime) minTime = data[i].start;
59
+ if (data[i].end > maxTime) maxTime = data[i].end;
60
+ }
61
+ var timeRange = maxTime - minTime || 1;
62
+
63
+ var svgHeight = data.length * ROW_STEP;
64
+ var maxHeight = 300;
65
+
66
+ return (
67
+ <div className="relative overflow-y-auto overflow-x-hidden" style={{ maxHeight: maxHeight }}>
68
+ <svg width="100%" height={svgHeight} className="block" viewBox={"0 0 600 " + svgHeight} preserveAspectRatio="none">
69
+ {data.map(function (d, idx) {
70
+ var barWidth = 600 - LEFT_MARGIN - RIGHT_MARGIN;
71
+ var x1 = LEFT_MARGIN + ((d.start - minTime) / timeRange) * barWidth;
72
+ var x2 = LEFT_MARGIN + ((d.end - minTime) / timeRange) * barWidth;
73
+ var w = Math.max(x2 - x1, 3);
74
+ var y = idx * ROW_STEP;
75
+
76
+ return (
77
+ <rect
78
+ key={d.id}
79
+ x={x1}
80
+ y={y}
81
+ width={w}
82
+ height={ROW_HEIGHT}
83
+ rx={3}
84
+ ry={3}
85
+ fill={getColor(d.project)}
86
+ opacity={0.7}
87
+ onMouseEnter={function (e) {
88
+ setHover({ x: e.clientX, y: e.clientY, datum: d });
89
+ }}
90
+ onMouseLeave={function () { setHover(null); }}
91
+ />
92
+ );
93
+ })}
94
+ </svg>
95
+
96
+ {hover && (
97
+ <div
98
+ className="fixed z-50 rounded-lg border border-base-content/8 bg-base-200 px-3 py-2 shadow-lg pointer-events-none max-w-[200px]"
99
+ style={{ left: hover.x + 12, top: hover.y - 50 }}
100
+ >
101
+ <p className="text-[10px] font-mono text-base-content/50 mb-1 truncate">{hover.datum.title}</p>
102
+ <div className="text-[11px] font-mono text-base-content/70 space-y-0.5">
103
+ <p><span className="text-base-content/40">project </span>{hover.datum.project}</p>
104
+ <p><span className="text-base-content/40">start </span>{formatTime(hover.datum.start)}</p>
105
+ <p><span className="text-base-content/40">end </span>{formatTime(hover.datum.end)}</p>
106
+ <p><span className="text-base-content/40">cost </span>${hover.datum.cost.toFixed(4)}</p>
107
+ </div>
108
+ </div>
109
+ )}
110
+ </div>
111
+ );
112
+ }
@@ -0,0 +1,82 @@
1
+ import {
2
+ AreaChart,
3
+ Area,
4
+ XAxis,
5
+ YAxis,
6
+ CartesianGrid,
7
+ Tooltip,
8
+ ResponsiveContainer,
9
+ } from "recharts";
10
+
11
+ var TICK_STYLE = {
12
+ fontSize: 10,
13
+ fontFamily: "var(--font-mono)",
14
+ fill: "oklch(0.9 0.02 280 / 0.3)",
15
+ };
16
+
17
+ var GRID_COLOR = "oklch(0.9 0.02 280 / 0.06)";
18
+
19
+ interface TokenFlowDatum {
20
+ date: string;
21
+ input: number;
22
+ output: number;
23
+ cacheRead: number;
24
+ }
25
+
26
+ interface TokenFlowChartProps {
27
+ data: TokenFlowDatum[];
28
+ }
29
+
30
+ function CustomTooltip({ active, payload, label }: { active?: boolean; payload?: Array<{ name: string; value: number; color: string }>; label?: string }) {
31
+ if (!active || !payload || payload.length === 0) return null;
32
+ return (
33
+ <div className="rounded-lg border border-base-content/8 bg-base-200 px-3 py-2 shadow-lg">
34
+ <p className="text-[10px] font-mono text-base-content/50 mb-1">{label}</p>
35
+ {payload.map(function (entry) {
36
+ return (
37
+ <div key={entry.name} className="flex items-center gap-2 text-[11px] font-mono">
38
+ <span className="inline-block w-2 h-2 rounded-full" style={{ background: entry.color }} />
39
+ <span className="text-base-content/60 capitalize">{entry.name}</span>
40
+ <span className="text-base-content ml-auto pl-4">{entry.value.toLocaleString()}</span>
41
+ </div>
42
+ );
43
+ })}
44
+ </div>
45
+ );
46
+ }
47
+
48
+ function formatTokens(v: number): string {
49
+ if (v >= 1000000) return (v / 1000000).toFixed(1) + "M";
50
+ if (v >= 1000) return (v / 1000).toFixed(0) + "k";
51
+ return String(v);
52
+ }
53
+
54
+ export function TokenFlowChart({ data }: TokenFlowChartProps) {
55
+ return (
56
+ <ResponsiveContainer width="100%" height={200}>
57
+ <AreaChart data={data} margin={{ top: 4, right: 4, left: -20, bottom: 0 }}>
58
+ <defs>
59
+ <linearGradient id="inputGrad" x1="0" y1="0" x2="0" y2="1">
60
+ <stop offset="5%" stopColor="oklch(55% 0.25 280)" stopOpacity={0.4} />
61
+ <stop offset="95%" stopColor="oklch(55% 0.25 280)" stopOpacity={0.05} />
62
+ </linearGradient>
63
+ <linearGradient id="outputGrad" x1="0" y1="0" x2="0" y2="1">
64
+ <stop offset="5%" stopColor="#22c55e" stopOpacity={0.4} />
65
+ <stop offset="95%" stopColor="#22c55e" stopOpacity={0.05} />
66
+ </linearGradient>
67
+ <linearGradient id="cacheReadGrad" x1="0" y1="0" x2="0" y2="1">
68
+ <stop offset="5%" stopColor="#f59e0b" stopOpacity={0.4} />
69
+ <stop offset="95%" stopColor="#f59e0b" stopOpacity={0.05} />
70
+ </linearGradient>
71
+ </defs>
72
+ <CartesianGrid strokeDasharray="3 3" stroke={GRID_COLOR} vertical={false} />
73
+ <XAxis dataKey="date" tick={TICK_STYLE} axisLine={false} tickLine={false} />
74
+ <YAxis tick={TICK_STYLE} axisLine={false} tickLine={false} tickFormatter={formatTokens} />
75
+ <Tooltip content={<CustomTooltip />} />
76
+ <Area type="monotone" dataKey="input" stackId="1" stroke="oklch(55% 0.25 280)" fill="url(#inputGrad)" strokeWidth={1.5} />
77
+ <Area type="monotone" dataKey="output" stackId="1" stroke="#22c55e" fill="url(#outputGrad)" strokeWidth={1.5} />
78
+ <Area type="monotone" dataKey="cacheRead" stackId="1" stroke="#f59e0b" fill="url(#cacheReadGrad)" strokeWidth={1.5} />
79
+ </AreaChart>
80
+ </ResponsiveContainer>
81
+ );
82
+ }
@@ -0,0 +1,89 @@
1
+ import { Sankey, Tooltip, ResponsiveContainer } from "recharts";
2
+
3
+ var NODE_COLORS: Record<string, string> = {
4
+ "Input Tokens": "oklch(55% 0.25 280)",
5
+ "Cache Read": "#f59e0b",
6
+ "Cache Creation": "oklch(65% 0.2 240)",
7
+ "Opus": "#a855f7",
8
+ "Sonnet": "oklch(55% 0.25 280)",
9
+ "Haiku": "#22c55e",
10
+ "Other": "#f59e0b",
11
+ "Output Tokens": "#22c55e",
12
+ };
13
+
14
+ interface SankeyData {
15
+ nodes: Array<{ name: string }>;
16
+ links: Array<{ source: number; target: number; value: number }>;
17
+ }
18
+
19
+ interface TokenSankeyChartProps {
20
+ data: SankeyData;
21
+ }
22
+
23
+ function CustomTooltip({ active, payload }: { active?: boolean; payload?: Array<{ payload: { source?: { name: string }; target?: { name: string }; value?: number; name?: string } }> }) {
24
+ if (!active || !payload || payload.length === 0) return null;
25
+ var d = payload[0].payload;
26
+ if (d.source && d.target) {
27
+ return (
28
+ <div className="rounded-lg border border-base-content/8 bg-base-200 px-3 py-2 shadow-lg">
29
+ <p className="text-[11px] font-mono text-base-content">
30
+ {d.source.name} → {d.target.name}: {(d.value || 0).toLocaleString()}
31
+ </p>
32
+ </div>
33
+ );
34
+ }
35
+ if (d.name) {
36
+ return (
37
+ <div className="rounded-lg border border-base-content/8 bg-base-200 px-3 py-2 shadow-lg">
38
+ <p className="text-[11px] font-mono text-base-content">{d.name}</p>
39
+ </div>
40
+ );
41
+ }
42
+ return null;
43
+ }
44
+
45
+ function SankeyNode({ x, y, width, height, index, payload }: { x: number; y: number; width: number; height: number; index: number; payload: { name: string } }) {
46
+ var color = NODE_COLORS[payload.name] || "oklch(0.5 0.1 280)";
47
+ return (
48
+ <g>
49
+ <rect x={x} y={y} width={width} height={height} fill={color} fillOpacity={0.85} rx={2} />
50
+ {height > 14 && (
51
+ <text
52
+ x={x + width + 6}
53
+ y={y + height / 2}
54
+ dy={4}
55
+ fill="oklch(0.9 0.02 280 / 0.5)"
56
+ fontSize={9}
57
+ fontFamily="var(--font-mono)"
58
+ >
59
+ {payload.name}
60
+ </text>
61
+ )}
62
+ </g>
63
+ );
64
+ }
65
+
66
+ export function TokenSankeyChart({ data }: TokenSankeyChartProps) {
67
+ if (!data.links || data.links.length === 0) {
68
+ return (
69
+ <div className="flex items-center justify-center h-[250px] text-base-content/30 font-mono text-[12px]">
70
+ No token flow data
71
+ </div>
72
+ );
73
+ }
74
+
75
+ return (
76
+ <ResponsiveContainer width="100%" height={250}>
77
+ <Sankey
78
+ data={data}
79
+ node={<SankeyNode x={0} y={0} width={0} height={0} index={0} payload={{ name: "" }} />}
80
+ link={{ stroke: "oklch(0.9 0.02 280 / 0.1)" }}
81
+ margin={{ top: 10, right: 100, left: 10, bottom: 10 }}
82
+ nodeWidth={12}
83
+ nodePadding={14}
84
+ >
85
+ <Tooltip content={<CustomTooltip />} />
86
+ </Sankey>
87
+ </ResponsiveContainer>
88
+ );
89
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cryptiklemur/lattice",
3
- "version": "1.5.0",
3
+ "version": "1.7.0",
4
4
  "description": "Multi-machine agentic dashboard for Claude Code. Monitor sessions, manage MCP servers and skills, orchestrate across mesh-networked nodes.",
5
5
  "license": "MIT",
6
6
  "author": "Aaron Scherer <me@aaronscherer.me>",
@@ -5,6 +5,18 @@ import type { AnalyticsPayload, AnalyticsPeriod, AnalyticsScope } from "@lattice
5
5
  import { estimateCost, projectPathToHash } from "../project/session";
6
6
  import { loadConfig } from "../config";
7
7
 
8
+ interface ResponseTimeDatum {
9
+ tokens: number;
10
+ duration: number;
11
+ model: string;
12
+ }
13
+
14
+ interface ContextMessage {
15
+ messageIndex: number;
16
+ inputTokens: number;
17
+ model: string;
18
+ }
19
+
8
20
  interface SessionData {
9
21
  id: string;
10
22
  title: string;
@@ -18,6 +30,8 @@ interface SessionData {
18
30
  tools: Map<string, number>;
19
31
  startTime: number;
20
32
  endTime: number;
33
+ responseTimePoints: ResponseTimeDatum[];
34
+ contextMessages: ContextMessage[];
21
35
  }
22
36
 
23
37
  interface CacheEntry {
@@ -83,8 +97,13 @@ function parseSessionFile(filePath: string, sessionId: string, projectSlug: stri
83
97
  tools: new Map(),
84
98
  startTime: 0,
85
99
  endTime: 0,
100
+ responseTimePoints: [],
101
+ contextMessages: [],
86
102
  };
87
103
 
104
+ var lastUserTimestamp = 0;
105
+ var assistantIndex = 0;
106
+
88
107
  for (var i = 0; i < lines.length; i++) {
89
108
  var line = lines[i].trim();
90
109
  if (!line) continue;
@@ -136,6 +155,16 @@ function parseSessionFile(filePath: string, sessionId: string, projectSlug: stri
136
155
  } else {
137
156
  data.models.set(bucket, { cost: cost, tokens: inTok + outTok });
138
157
  }
158
+
159
+ if (outTok > 0 && timestamp > 0 && lastUserTimestamp > 0) {
160
+ var dur = timestamp - lastUserTimestamp;
161
+ if (dur > 0 && dur < 600000) {
162
+ data.responseTimePoints.push({ tokens: outTok, duration: dur, model: bucket });
163
+ }
164
+ }
165
+
166
+ data.contextMessages.push({ messageIndex: assistantIndex, inputTokens: inTok + cacheRead + cacheCreation, model: bucket });
167
+ assistantIndex++;
139
168
  }
140
169
 
141
170
  if (!data.title && message.content) {
@@ -144,6 +173,7 @@ function parseSessionFile(filePath: string, sessionId: string, projectSlug: stri
144
173
  }
145
174
  }
146
175
  } else if (parsed.type === "user") {
176
+ if (timestamp > 0) lastUserTimestamp = timestamp;
147
177
  var userMsg = parsed.message as Record<string, unknown> | undefined;
148
178
  if (!userMsg || !Array.isArray(userMsg.content)) continue;
149
179
 
@@ -408,6 +438,206 @@ function aggregate(sessions: SessionData[], period: AnalyticsPeriod): AnalyticsP
408
438
  });
409
439
  toolUsage.sort(function (a, b) { return b.count - a.count; });
410
440
 
441
+ var responseTimeData: AnalyticsPayload["responseTimeData"] = [];
442
+ for (var rti = 0; rti < filtered.length; rti++) {
443
+ var rtSess = filtered[rti];
444
+ for (var rtj = 0; rtj < rtSess.responseTimePoints.length; rtj++) {
445
+ var rtp = rtSess.responseTimePoints[rtj];
446
+ responseTimeData.push({ tokens: rtp.tokens, duration: rtp.duration, model: rtp.model, sessionId: rtSess.id });
447
+ }
448
+ if (responseTimeData.length >= 200) break;
449
+ }
450
+ if (responseTimeData.length > 200) responseTimeData.length = 200;
451
+
452
+ var contextWindowSizes: Record<string, number> = { opus: 200000, sonnet: 200000, haiku: 200000, other: 200000 };
453
+ var contextUtilization: AnalyticsPayload["contextUtilization"] = [];
454
+ var recentSessions = sorted.slice(0, 5);
455
+ for (var cui = 0; cui < recentSessions.length; cui++) {
456
+ var cuSess = recentSessions[cui];
457
+ var runningTokens = 0;
458
+ var primaryModel = "other";
459
+ var maxModelTokens = 0;
460
+ cuSess.models.forEach(function (val, key) {
461
+ if (val.tokens > maxModelTokens) {
462
+ maxModelTokens = val.tokens;
463
+ primaryModel = key;
464
+ }
465
+ });
466
+ var windowSize = contextWindowSizes[primaryModel] || 200000;
467
+ for (var cmj = 0; cmj < cuSess.contextMessages.length; cmj++) {
468
+ var cm = cuSess.contextMessages[cmj];
469
+ runningTokens += cm.inputTokens;
470
+ contextUtilization.push({
471
+ messageIndex: cm.messageIndex,
472
+ contextPercent: Math.min((runningTokens / windowSize) * 100, 100),
473
+ sessionId: cuSess.id,
474
+ title: cuSess.title,
475
+ });
476
+ }
477
+ }
478
+
479
+ var sankeyNodes = [
480
+ { name: "Input Tokens" },
481
+ { name: "Cache Read" },
482
+ { name: "Cache Creation" },
483
+ { name: "Opus" },
484
+ { name: "Sonnet" },
485
+ { name: "Haiku" },
486
+ { name: "Other" },
487
+ { name: "Output Tokens" },
488
+ ];
489
+ var modelNodeMap: Record<string, number> = { opus: 3, sonnet: 4, haiku: 5, other: 6 };
490
+ var sankeyLinks: Array<{ source: number; target: number; value: number }> = [];
491
+ var modelInputTotals = new Map<string, number>();
492
+ var modelCacheTotals = new Map<string, number>();
493
+ var modelCacheCreationTotals = new Map<string, number>();
494
+ var modelOutputTotals = new Map<string, number>();
495
+
496
+ for (var ski = 0; ski < filtered.length; ski++) {
497
+ var skSess = filtered[ski];
498
+ var skTotal = skSess.inputTokens + skSess.cacheReadTokens + skSess.cacheCreationTokens;
499
+ if (skTotal === 0) continue;
500
+ skSess.models.forEach(function (val, key) {
501
+ var proportion = val.tokens / (skTotal + skSess.outputTokens || 1);
502
+ modelInputTotals.set(key, (modelInputTotals.get(key) || 0) + skSess.inputTokens * proportion);
503
+ modelCacheTotals.set(key, (modelCacheTotals.get(key) || 0) + skSess.cacheReadTokens * proportion);
504
+ modelCacheCreationTotals.set(key, (modelCacheCreationTotals.get(key) || 0) + skSess.cacheCreationTokens * proportion);
505
+ modelOutputTotals.set(key, (modelOutputTotals.get(key) || 0) + skSess.outputTokens * proportion);
506
+ });
507
+ }
508
+
509
+ ["opus", "sonnet", "haiku", "other"].forEach(function (model) {
510
+ var nodeIdx = modelNodeMap[model];
511
+ var inputVal = Math.round(modelInputTotals.get(model) || 0);
512
+ var cacheVal = Math.round(modelCacheTotals.get(model) || 0);
513
+ var cacheCreationVal = Math.round(modelCacheCreationTotals.get(model) || 0);
514
+ var outputVal = Math.round(modelOutputTotals.get(model) || 0);
515
+ if (inputVal > 0) sankeyLinks.push({ source: 0, target: nodeIdx, value: inputVal });
516
+ if (cacheVal > 0) sankeyLinks.push({ source: 1, target: nodeIdx, value: cacheVal });
517
+ if (cacheCreationVal > 0) sankeyLinks.push({ source: 2, target: nodeIdx, value: cacheCreationVal });
518
+ if (outputVal > 0) sankeyLinks.push({ source: nodeIdx, target: 7, value: outputVal });
519
+ });
520
+
521
+ var tokenFlowSankey: AnalyticsPayload["tokenFlowSankey"] = { nodes: sankeyNodes, links: sankeyLinks };
522
+
523
+ var activityCalendarMap = new Map<string, { count: number; tokens: number; cost: number }>();
524
+ for (var aci = 0; aci < filtered.length; aci++) {
525
+ var acSess = filtered[aci];
526
+ var acDate = formatDate(acSess.endTime > 0 ? acSess.endTime : acSess.startTime);
527
+ var acEntry = activityCalendarMap.get(acDate);
528
+ if (!acEntry) {
529
+ acEntry = { count: 0, tokens: 0, cost: 0 };
530
+ activityCalendarMap.set(acDate, acEntry);
531
+ }
532
+ acEntry.count++;
533
+ acEntry.tokens += acSess.inputTokens + acSess.outputTokens;
534
+ acEntry.cost += acSess.cost;
535
+ }
536
+
537
+ var activityCalendar: AnalyticsPayload["activityCalendar"] = [];
538
+ if (dates.length > 0) {
539
+ var calStart = new Date(dates[0]);
540
+ var calEnd = new Date(dates[dates.length - 1]);
541
+ var calCursor = new Date(calStart);
542
+ while (calCursor <= calEnd) {
543
+ var calKey = formatDate(calCursor.getTime());
544
+ var calData = activityCalendarMap.get(calKey);
545
+ activityCalendar.push({
546
+ date: calKey,
547
+ count: calData ? calData.count : 0,
548
+ tokens: calData ? calData.tokens : 0,
549
+ cost: calData ? calData.cost : 0,
550
+ });
551
+ calCursor.setDate(calCursor.getDate() + 1);
552
+ }
553
+ }
554
+
555
+ var hourlyHeatmap: AnalyticsPayload["hourlyHeatmap"] = [];
556
+ var heatmapGrid = new Map<string, number>();
557
+ for (var hmi = 0; hmi < filtered.length; hmi++) {
558
+ var hmSess = filtered[hmi];
559
+ if (hmSess.startTime <= 0) continue;
560
+ var hmDate = new Date(hmSess.startTime);
561
+ var hmDay = hmDate.getDay();
562
+ var hmHour = hmDate.getHours();
563
+ var hmKey = hmDay + ":" + hmHour;
564
+ heatmapGrid.set(hmKey, (heatmapGrid.get(hmKey) || 0) + 1);
565
+ }
566
+ for (var hd = 0; hd < 7; hd++) {
567
+ for (var hh = 0; hh < 24; hh++) {
568
+ var hhKey = hd + ":" + hh;
569
+ hourlyHeatmap.push({ day: hd, hour: hh, count: heatmapGrid.get(hhKey) || 0 });
570
+ }
571
+ }
572
+
573
+ var sessionTimeline: AnalyticsPayload["sessionTimeline"] = [];
574
+ var tlSorted = filtered
575
+ .filter(function (s) { return s.startTime > 0 && s.endTime > 0; })
576
+ .sort(function (a, b) { return b.startTime - a.startTime; });
577
+ var tlCap = Math.min(tlSorted.length, 50);
578
+ for (var tli = 0; tli < tlCap; tli++) {
579
+ var tlSess = tlSorted[tli];
580
+ sessionTimeline.push({
581
+ id: tlSess.id,
582
+ title: tlSess.title,
583
+ project: tlSess.project,
584
+ start: tlSess.startTime,
585
+ end: tlSess.endTime,
586
+ cost: tlSess.cost,
587
+ });
588
+ }
589
+
590
+ var dailySummaryMap = new Map<string, { sessions: number; cost: number; tokens: number; tools: Map<string, number>; models: Map<string, number> }>();
591
+ for (var dsi = 0; dsi < filtered.length; dsi++) {
592
+ var dsSess = filtered[dsi];
593
+ var dsDate = formatDate(dsSess.endTime > 0 ? dsSess.endTime : dsSess.startTime);
594
+ var dsEntry = dailySummaryMap.get(dsDate);
595
+ if (!dsEntry) {
596
+ dsEntry = { sessions: 0, cost: 0, tokens: 0, tools: new Map(), models: new Map() };
597
+ dailySummaryMap.set(dsDate, dsEntry);
598
+ }
599
+ dsEntry.sessions++;
600
+ dsEntry.cost += dsSess.cost;
601
+ dsEntry.tokens += dsSess.inputTokens + dsSess.outputTokens;
602
+ dsSess.tools.forEach(function (count, tool) {
603
+ dsEntry!.tools.set(tool, (dsEntry!.tools.get(tool) || 0) + count);
604
+ });
605
+ dsSess.models.forEach(function (val, key) {
606
+ dsEntry!.models.set(key, (dsEntry!.models.get(key) || 0) + val.cost);
607
+ });
608
+ }
609
+
610
+ var dailySummaries: AnalyticsPayload["dailySummaries"] = [];
611
+ var dsSortedDates = Array.from(dailySummaryMap.keys()).sort();
612
+ for (var dsdi = 0; dsdi < dsSortedDates.length; dsdi++) {
613
+ var dsd = dsSortedDates[dsdi];
614
+ var dsData = dailySummaryMap.get(dsd)!;
615
+ var topTool = "";
616
+ var topToolCount = 0;
617
+ dsData.tools.forEach(function (count, tool) {
618
+ if (count > topToolCount) {
619
+ topToolCount = count;
620
+ topTool = tool;
621
+ }
622
+ });
623
+ var modelMix: Record<string, number> = {};
624
+ var modelTotal = 0;
625
+ dsData.models.forEach(function (cost) { modelTotal += cost; });
626
+ if (modelTotal > 0) {
627
+ dsData.models.forEach(function (cost, model) {
628
+ modelMix[model] = Math.round((cost / modelTotal) * 100) / 100;
629
+ });
630
+ }
631
+ dailySummaries.push({
632
+ date: dsd,
633
+ sessions: dsData.sessions,
634
+ cost: dsData.cost,
635
+ tokens: dsData.tokens,
636
+ topTool: topTool,
637
+ modelMix: modelMix,
638
+ });
639
+ }
640
+
411
641
  return {
412
642
  totalCost: totalCost,
413
643
  totalSessions: filtered.length,
@@ -430,6 +660,13 @@ function aggregate(sessions: SessionData[], period: AnalyticsPeriod): AnalyticsP
430
660
  modelUsage: modelUsage,
431
661
  projectBreakdown: projectBreakdown,
432
662
  toolUsage: toolUsage,
663
+ responseTimeData: responseTimeData,
664
+ contextUtilization: contextUtilization,
665
+ tokenFlowSankey: tokenFlowSankey,
666
+ activityCalendar: activityCalendar,
667
+ hourlyHeatmap: hourlyHeatmap,
668
+ sessionTimeline: sessionTimeline,
669
+ dailySummaries: dailySummaries,
433
670
  };
434
671
  }
435
672