@cryptiklemur/lattice 1.11.0 → 1.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,5 @@
1
1
  import { Component } from "react";
2
+ import { BarChart3 } from "lucide-react";
2
3
  import { useAnalytics } from "../../hooks/useAnalytics";
3
4
  import { useMesh } from "../../hooks/useMesh";
4
5
 
@@ -21,8 +22,20 @@ class ChartErrorBoundary extends Component<{ children: React.ReactNode; name: st
21
22
  return this.props.children;
22
23
  }
23
24
  }
25
+
26
+ function SectionHeader(props: { label: string }) {
27
+ return (
28
+ <div className="flex items-center gap-3 pt-4 pb-1">
29
+ <div className="h-px flex-1 bg-base-content/6" />
30
+ <span className="text-[9px] font-mono font-bold uppercase tracking-[0.15em] text-base-content/20">{props.label}</span>
31
+ <div className="h-px flex-1 bg-base-content/6" />
32
+ </div>
33
+ );
34
+ }
35
+
24
36
  import { PeriodSelector } from "./PeriodSelector";
25
37
  import { ChartCard } from "./ChartCard";
38
+ import { QuickStats } from "./QuickStats";
26
39
  import { CostAreaChart } from "./charts/CostAreaChart";
27
40
  import { CumulativeCostChart } from "./charts/CumulativeCostChart";
28
41
  import { CostDonutChart } from "./charts/CostDonutChart";
@@ -58,7 +71,18 @@ export function AnalyticsView() {
58
71
 
59
72
  <div className="flex-1 overflow-y-auto px-6 py-4">
60
73
  {analytics.loading && !analytics.data && (
61
- <div className="text-center text-base-content/30 py-20 font-mono text-[13px]">Loading analytics...</div>
74
+ <div className="flex flex-col gap-4 max-w-[1200px] mx-auto">
75
+ <div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
76
+ {[0, 1, 2, 3].map(function (i) {
77
+ return <div key={i} className="h-24 rounded-xl bg-base-content/[0.03] animate-pulse" />;
78
+ })}
79
+ </div>
80
+ <div className="h-[240px] rounded-xl bg-base-content/[0.03] animate-pulse" />
81
+ <div className="grid grid-cols-2 gap-4">
82
+ <div className="h-[240px] rounded-xl bg-base-content/[0.03] animate-pulse" />
83
+ <div className="h-[240px] rounded-xl bg-base-content/[0.03] animate-pulse" />
84
+ </div>
85
+ </div>
62
86
  )}
63
87
 
64
88
  {analytics.error && (
@@ -67,6 +91,10 @@ export function AnalyticsView() {
67
91
 
68
92
  {analytics.data && (
69
93
  <div className="flex flex-col gap-4 max-w-[1200px] mx-auto pb-8">
94
+ <QuickStats />
95
+
96
+ <SectionHeader label="Cost" />
97
+
70
98
  <ChartCard title="Cost Over Time">
71
99
  <CostAreaChart data={analytics.data.costOverTime} />
72
100
  </ChartCard>
@@ -89,6 +117,8 @@ export function AnalyticsView() {
89
117
  </ChartCard>
90
118
  </div>
91
119
 
120
+ <SectionHeader label="Tokens & Performance" />
121
+
92
122
  <ChartCard title="Token Flow">
93
123
  <ChartErrorBoundary name="TokenFlow">
94
124
  <TokenFlowChart data={analytics.data.tokensOverTime} />
@@ -121,6 +151,8 @@ export function AnalyticsView() {
121
151
  </ChartCard>
122
152
  </div>
123
153
 
154
+ <SectionHeader label="Activity" />
155
+
124
156
  <ChartCard title="Activity Calendar">
125
157
  <ChartErrorBoundary name="Calendar">
126
158
  <ActivityCalendar data={analytics.data.activityCalendar} />
@@ -146,6 +178,8 @@ export function AnalyticsView() {
146
178
  </ChartErrorBoundary>
147
179
  </ChartCard>
148
180
 
181
+ <SectionHeader label="Tools & Projects" />
182
+
149
183
  <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
150
184
  <ChartCard title="Tool Usage (Treemap)">
151
185
  <ChartErrorBoundary name="Treemap">
@@ -178,6 +212,8 @@ export function AnalyticsView() {
178
212
  </ChartErrorBoundary>
179
213
  </ChartCard>
180
214
 
215
+ <SectionHeader label="Fleet" />
216
+
181
217
  <ChartCard title="Node Fleet">
182
218
  <ChartErrorBoundary name="Fleet">
183
219
  <NodeFleetOverview nodes={nodes} />
@@ -187,7 +223,13 @@ export function AnalyticsView() {
187
223
  )}
188
224
 
189
225
  {!analytics.loading && !analytics.error && !analytics.data && (
190
- <div className="text-center text-base-content/30 py-20 font-mono text-[13px]">No analytics data yet</div>
226
+ <div className="flex flex-col items-center justify-center py-24 gap-4">
227
+ <BarChart3 size={40} className="text-base-content/10" />
228
+ <div className="text-center">
229
+ <p className="text-[14px] font-mono text-base-content/40">No analytics data yet</p>
230
+ <p className="text-[12px] text-base-content/25 mt-1">Start a Claude session to begin tracking usage</p>
231
+ </div>
232
+ </div>
191
233
  )}
192
234
  </div>
193
235
  </div>
@@ -2,12 +2,39 @@ import { useState, useEffect, useRef, createContext, useContext } from "react";
2
2
  import { Maximize2, Minimize2 } from "lucide-react";
3
3
  import type { ReactNode } from "react";
4
4
 
5
- var ChartFullscreenContext = createContext(false);
5
+ var ChartFullscreenContext = createContext<number | false>(false);
6
6
 
7
- export function useChartFullscreen(): boolean {
7
+ export function useChartFullscreen(): number | false {
8
8
  return useContext(ChartFullscreenContext);
9
9
  }
10
10
 
11
+ function FullscreenChartArea(props: { children: ReactNode }) {
12
+ var containerRef = useRef<HTMLDivElement>(null);
13
+ var [height, setHeight] = useState(400);
14
+
15
+ useEffect(function () {
16
+ if (!containerRef.current) return;
17
+ var observer = new ResizeObserver(function (entries) {
18
+ for (var i = 0; i < entries.length; i++) {
19
+ var h = entries[i].contentRect.height;
20
+ if (h > 0) setHeight(h);
21
+ }
22
+ });
23
+ observer.observe(containerRef.current);
24
+ var h = containerRef.current.clientHeight;
25
+ if (h > 0) setHeight(h);
26
+ return function () { observer.disconnect(); };
27
+ }, []);
28
+
29
+ return (
30
+ <div ref={containerRef} className="flex-1 p-6 overflow-auto min-h-0">
31
+ <ChartFullscreenContext.Provider value={height - 48}>
32
+ {props.children}
33
+ </ChartFullscreenContext.Provider>
34
+ </div>
35
+ );
36
+ }
37
+
11
38
  interface ChartCardProps {
12
39
  title: string;
13
40
  children: ReactNode;
@@ -77,7 +104,7 @@ export function ChartCard(props: ChartCardProps) {
77
104
  openFullscreen();
78
105
  }
79
106
  }}
80
- className="text-base-content/20 hover:text-base-content/50 transition-colors cursor-pointer p-0.5 rounded hover:bg-base-content/5"
107
+ className="opacity-0 group-hover:opacity-100 text-base-content/20 hover:text-base-content/50 transition-all duration-200 cursor-pointer p-0.5 rounded hover:bg-base-content/5"
81
108
  aria-label={isFullscreen ? "Exit fullscreen" : "Fullscreen"}
82
109
  title={isFullscreen ? "Exit fullscreen (Esc)" : "Fullscreen"}
83
110
  >
@@ -150,11 +177,9 @@ export function ChartCard(props: ChartCardProps) {
150
177
  </button>
151
178
  </div>
152
179
  </div>
153
- <div className="flex-1 p-6 overflow-auto min-h-0 fullscreen-chart-container">
154
- <ChartFullscreenContext.Provider value={true}>
155
- {props.children}
156
- </ChartFullscreenContext.Provider>
157
- </div>
180
+ <FullscreenChartArea>
181
+ {props.children}
182
+ </FullscreenChartArea>
158
183
  </div>
159
184
  </div>
160
185
  </>
@@ -164,7 +189,7 @@ export function ChartCard(props: ChartCardProps) {
164
189
  return (
165
190
  <div
166
191
  ref={cardRef}
167
- className={"rounded-xl border border-base-content/8 bg-base-300/50 p-4 " + (props.className || "")}
192
+ className={"group rounded-xl border border-base-content/8 bg-base-300/50 p-4 cursor-pointer hover:border-base-content/12 transition-all duration-200 " + (props.className || "")}
168
193
  >
169
194
  {cardContent}
170
195
  </div>
@@ -37,13 +37,13 @@ function CustomTooltip({ active, payload, label }: { active?: boolean; payload?:
37
37
  }
38
38
 
39
39
  export function CacheEfficiencyChart({ data }: CacheEfficiencyChartProps) {
40
- var isFullscreen = useChartFullscreen();
40
+ var fullscreenHeight = useChartFullscreen();
41
41
  var displayData = data.map(function (d) {
42
42
  return { date: d.date, rate: d.rate * 100 };
43
43
  });
44
44
 
45
45
  return (
46
- <ResponsiveContainer width="100%" height={isFullscreen ? "100%" : 200}>
46
+ <ResponsiveContainer width="100%" height={fullscreenHeight || 200}>
47
47
  <AreaChart data={displayData} margin={{ top: 4, right: 4, left: -20, bottom: 0 }}>
48
48
  <defs>
49
49
  <linearGradient id="cacheEffGrad" x1="0" y1="0" x2="0" y2="1">
@@ -54,7 +54,7 @@ function CustomTooltip({ active, payload }: { active?: boolean; payload?: Array<
54
54
  }
55
55
 
56
56
  export function ContextUtilizationChart({ data }: ContextUtilizationChartProps) {
57
- var isFullscreen = useChartFullscreen();
57
+ var fullscreenHeight = useChartFullscreen();
58
58
  var sessionMap = new Map<string, { title: string; points: Array<{ messageIndex: number; contextPercent: number }> }>();
59
59
  for (var i = 0; i < data.length; i++) {
60
60
  var d = data[i];
@@ -86,7 +86,7 @@ export function ContextUtilizationChart({ data }: ContextUtilizationChartProps)
86
86
  }
87
87
 
88
88
  return (
89
- <ResponsiveContainer width="100%" height={isFullscreen ? "100%" : 200}>
89
+ <ResponsiveContainer width="100%" height={fullscreenHeight || 200}>
90
90
  <LineChart data={merged} margin={{ top: 4, right: 4, left: -20, bottom: 0 }}>
91
91
  <CartesianGrid strokeDasharray="3 3" stroke={GRID_COLOR} vertical={false} />
92
92
  <XAxis dataKey="messageIndex" tick={TICK_STYLE} axisLine={false} tickLine={false} />
@@ -49,9 +49,9 @@ function CustomTooltip({ active, payload, label }: { active?: boolean; payload?:
49
49
  }
50
50
 
51
51
  export function CostAreaChart({ data }: CostAreaChartProps) {
52
- var isFullscreen = useChartFullscreen();
52
+ var fullscreenHeight = useChartFullscreen();
53
53
  return (
54
- <ResponsiveContainer width="100%" height={isFullscreen ? "100%" : 200}>
54
+ <ResponsiveContainer width="100%" height={fullscreenHeight || 200}>
55
55
  <AreaChart data={data} margin={{ top: 4, right: 4, left: -20, bottom: 0 }}>
56
56
  <defs>
57
57
  <linearGradient id="opusGrad" x1="0" y1="0" x2="0" y2="1">
@@ -37,9 +37,9 @@ function CustomTooltip({ active, payload, label }: { active?: boolean; payload?:
37
37
  }
38
38
 
39
39
  export function CostDistributionChart({ data }: CostDistributionChartProps) {
40
- var isFullscreen = useChartFullscreen();
40
+ var fullscreenHeight = useChartFullscreen();
41
41
  return (
42
- <ResponsiveContainer width="100%" height={isFullscreen ? "100%" : 200}>
42
+ <ResponsiveContainer width="100%" height={fullscreenHeight || 200}>
43
43
  <AreaChart data={data} margin={{ top: 4, right: 4, left: -20, bottom: 0 }}>
44
44
  <defs>
45
45
  <linearGradient id="distGrad" x1="0" y1="0" x2="0" y2="1">
@@ -53,10 +53,10 @@ function CenterLabel({ totalCost }: { totalCost: number }) {
53
53
  }
54
54
 
55
55
  export function CostDonutChart({ modelUsage, totalCost }: CostDonutChartProps) {
56
- var isFullscreen = useChartFullscreen();
56
+ var fullscreenHeight = useChartFullscreen();
57
57
  return (
58
58
  <div>
59
- <ResponsiveContainer width="100%" height={isFullscreen ? "100%" : 200}>
59
+ <ResponsiveContainer width="100%" height={fullscreenHeight || 200}>
60
60
  <PieChart>
61
61
  <Pie
62
62
  data={modelUsage}
@@ -37,9 +37,9 @@ function CustomTooltip({ active, payload, label }: { active?: boolean; payload?:
37
37
  }
38
38
 
39
39
  export function CumulativeCostChart({ data }: CumulativeCostChartProps) {
40
- var isFullscreen = useChartFullscreen();
40
+ var fullscreenHeight = useChartFullscreen();
41
41
  return (
42
- <ResponsiveContainer width="100%" height={isFullscreen ? "100%" : 200}>
42
+ <ResponsiveContainer width="100%" height={fullscreenHeight || 200}>
43
43
  <AreaChart data={data} margin={{ top: 4, right: 4, left: -20, bottom: 0 }}>
44
44
  <defs>
45
45
  <linearGradient id="cumulativeGrad" x1="0" y1="0" x2="0" y2="1">
@@ -50,7 +50,7 @@ function CustomTooltip({ active, payload }: { active?: boolean; payload?: Array<
50
50
  }
51
51
 
52
52
  export function ResponseTimeScatter({ data }: ResponseTimeScatterProps) {
53
- var isFullscreen = useChartFullscreen();
53
+ var fullscreenHeight = useChartFullscreen();
54
54
  var models = Array.from(new Set(data.map(function (d) { return d.model; })));
55
55
 
56
56
  var byModel = models.map(function (model) {
@@ -64,7 +64,7 @@ export function ResponseTimeScatter({ data }: ResponseTimeScatterProps) {
64
64
  });
65
65
 
66
66
  return (
67
- <ResponsiveContainer width="100%" height={isFullscreen ? "100%" : 200}>
67
+ <ResponsiveContainer width="100%" height={fullscreenHeight || 200}>
68
68
  <ScatterChart margin={{ top: 4, right: 4, left: -20, bottom: 0 }}>
69
69
  <CartesianGrid strokeDasharray="3 3" stroke={GRID_COLOR} />
70
70
  <XAxis
@@ -63,7 +63,7 @@ function CustomTooltip({ active, payload }: { active?: boolean; payload?: Array<
63
63
  }
64
64
 
65
65
  export function SessionBubbleChart({ data }: SessionBubbleChartProps) {
66
- var isFullscreen = useChartFullscreen();
66
+ var fullscreenHeight = useChartFullscreen();
67
67
  var projects = Array.from(new Set(data.map(function (d) { return d.project; })));
68
68
 
69
69
  function getColor(project: string): string {
@@ -85,7 +85,7 @@ export function SessionBubbleChart({ data }: SessionBubbleChartProps) {
85
85
  var maxTs = Math.max(...data.map(function (d) { return d.timestamp; }));
86
86
 
87
87
  return (
88
- <ResponsiveContainer width="100%" height={isFullscreen ? "100%" : 200}>
88
+ <ResponsiveContainer width="100%" height={fullscreenHeight || 200}>
89
89
  <ScatterChart margin={{ top: 4, right: 4, left: -20, bottom: 0 }}>
90
90
  <CartesianGrid strokeDasharray="3 3" stroke={GRID_COLOR} />
91
91
  <XAxis
@@ -53,9 +53,9 @@ function formatTokens(v: number): string {
53
53
  }
54
54
 
55
55
  export function TokenFlowChart({ data }: TokenFlowChartProps) {
56
- var isFullscreen = useChartFullscreen();
56
+ var fullscreenHeight = useChartFullscreen();
57
57
  return (
58
- <ResponsiveContainer width="100%" height={isFullscreen ? "100%" : 200}>
58
+ <ResponsiveContainer width="100%" height={fullscreenHeight || 200}>
59
59
  <AreaChart data={data} margin={{ top: 4, right: 4, left: -20, bottom: 0 }}>
60
60
  <defs>
61
61
  <linearGradient id="inputGrad" x1="0" y1="0" x2="0" y2="1">
@@ -65,7 +65,7 @@ function SankeyNode({ x, y, width, height, index, payload }: { x: number; y: num
65
65
  }
66
66
 
67
67
  export function TokenSankeyChart({ data }: TokenSankeyChartProps) {
68
- var isFullscreen = useChartFullscreen();
68
+ var fullscreenHeight = useChartFullscreen();
69
69
  if (!data.links || data.links.length === 0) {
70
70
  return (
71
71
  <div className="flex items-center justify-center h-[250px] text-base-content/30 font-mono text-[12px]">
@@ -75,7 +75,7 @@ export function TokenSankeyChart({ data }: TokenSankeyChartProps) {
75
75
  }
76
76
 
77
77
  return (
78
- <ResponsiveContainer width="100%" height={isFullscreen ? "100%" : 250}>
78
+ <ResponsiveContainer width="100%" height={fullscreenHeight || 250}>
79
79
  <Sankey
80
80
  data={data}
81
81
  node={<SankeyNode x={0} y={0} width={0} height={0} index={0} payload={{ name: "" }} />}
@@ -77,7 +77,7 @@ function CustomTooltip({ active, payload }: { active?: boolean; payload?: Array<
77
77
  }
78
78
 
79
79
  export function ToolTreemap({ data }: ToolTreemapProps) {
80
- var isFullscreen = useChartFullscreen();
80
+ var fullscreenHeight = useChartFullscreen();
81
81
  if (!data || data.length === 0) {
82
82
  return (
83
83
  <div className="flex items-center justify-center h-[250px] text-base-content/25 font-mono text-[11px]">
@@ -96,7 +96,7 @@ export function ToolTreemap({ data }: ToolTreemapProps) {
96
96
  });
97
97
 
98
98
  return (
99
- <ResponsiveContainer width="100%" height={isFullscreen ? "100%" : 250}>
99
+ <ResponsiveContainer width="100%" height={fullscreenHeight || 250}>
100
100
  <Treemap
101
101
  data={treemapData}
102
102
  dataKey="size"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cryptiklemur/lattice",
3
- "version": "1.11.0",
3
+ "version": "1.11.1",
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>",