@cryptiklemur/lattice 1.22.3 → 1.23.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.
@@ -3,19 +3,25 @@ import { BarChart3 } from "lucide-react";
3
3
  import { useAnalytics } from "../../hooks/useAnalytics";
4
4
  import { useMesh } from "../../hooks/useMesh";
5
5
 
6
- class ChartErrorBoundary extends Component<{ children: React.ReactNode; name: string }, { error: Error | null }> {
6
+ class ChartErrorBoundary extends Component<{ children: React.ReactNode; name: string }, { hasError: boolean }> {
7
7
  constructor(props: { children: React.ReactNode; name: string }) {
8
8
  super(props);
9
- this.state = { error: null };
9
+ this.state = { hasError: false };
10
10
  }
11
- static getDerivedStateFromError(error: Error) {
12
- return { error: error };
11
+ static getDerivedStateFromError() {
12
+ return { hasError: true };
13
13
  }
14
14
  render() {
15
- if (this.state.error) {
15
+ if (this.state.hasError) {
16
16
  return (
17
- <div className="flex items-center justify-center h-[200px] text-base-content/25 font-mono text-[11px]">
18
- Chart error: {this.props.name}
17
+ <div className="flex flex-col items-center justify-center h-[200px] gap-3">
18
+ <span className="text-base-content/30 font-mono text-[12px]">Unable to load chart</span>
19
+ <button
20
+ onClick={() => this.setState({ hasError: false })}
21
+ className="text-[11px] font-mono text-primary/60 hover:text-primary transition-colors cursor-pointer px-3 py-1 rounded-md border border-primary/20 hover:border-primary/40"
22
+ >
23
+ Retry
24
+ </button>
19
25
  </div>
20
26
  );
21
27
  }
@@ -26,9 +32,9 @@ class ChartErrorBoundary extends Component<{ children: React.ReactNode; name: st
26
32
  function SectionHeader(props: { label: string }) {
27
33
  return (
28
34
  <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" />
35
+ <div className="h-px flex-1 bg-base-content/10" />
36
+ <span className="text-[9px] font-mono font-bold uppercase tracking-[0.15em] text-base-content/40">{props.label}</span>
37
+ <div className="h-px flex-1 bg-base-content/10" />
32
38
  </div>
33
39
  );
34
40
  }
@@ -55,9 +61,9 @@ import { SessionComplexityList } from "./charts/SessionComplexityList";
55
61
  import { NodeFleetOverview } from "./charts/NodeFleetOverview";
56
62
 
57
63
  export function AnalyticsView() {
58
- var analytics = useAnalytics();
59
- var mesh = useMesh();
60
- var nodes = mesh.nodes;
64
+ const analytics = useAnalytics();
65
+ const mesh = useMesh();
66
+ const nodes = mesh.nodes;
61
67
 
62
68
  return (
63
69
  <div className="flex flex-col h-full overflow-hidden bg-base-100 bg-lattice-grid">
@@ -98,24 +104,34 @@ export function AnalyticsView() {
98
104
  <SectionHeader label="Cost" />
99
105
 
100
106
  <ChartCard title="Cost Over Time">
101
- <CostAreaChart data={analytics.data.costOverTime} />
107
+ <ChartErrorBoundary name="CostArea">
108
+ <CostAreaChart data={analytics.data.costOverTime} />
109
+ </ChartErrorBoundary>
102
110
  </ChartCard>
103
111
 
104
112
  <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
105
113
  <ChartCard title="Cost Breakdown">
106
- <CostDonutChart modelUsage={analytics.data.modelUsage} totalCost={analytics.data.totalCost} />
114
+ <ChartErrorBoundary name="CostDonut">
115
+ <CostDonutChart modelUsage={analytics.data.modelUsage} totalCost={analytics.data.totalCost} />
116
+ </ChartErrorBoundary>
107
117
  </ChartCard>
108
118
  <ChartCard title="Cumulative Cost">
109
- <CumulativeCostChart data={analytics.data.cumulativeCost} />
119
+ <ChartErrorBoundary name="CumulativeCost">
120
+ <CumulativeCostChart data={analytics.data.cumulativeCost} />
121
+ </ChartErrorBoundary>
110
122
  </ChartCard>
111
123
  </div>
112
124
 
113
125
  <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
114
126
  <ChartCard title="Cost Distribution">
115
- <CostDistributionChart data={analytics.data.costDistribution} />
127
+ <ChartErrorBoundary name="CostDistribution">
128
+ <CostDistributionChart data={analytics.data.costDistribution} />
129
+ </ChartErrorBoundary>
116
130
  </ChartCard>
117
131
  <ChartCard title="Session Costs">
118
- <SessionBubbleChart data={analytics.data.sessionBubbles} />
132
+ <ChartErrorBoundary name="SessionBubble">
133
+ <SessionBubbleChart data={analytics.data.sessionBubbles} />
134
+ </ChartErrorBoundary>
119
135
  </ChartCard>
120
136
  </div>
121
137
 
@@ -182,13 +198,11 @@ export function AnalyticsView() {
182
198
 
183
199
  <SectionHeader label="Projects" />
184
200
 
185
- <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
186
- <ChartCard title="Project Comparison">
187
- <ChartErrorBoundary name="Radar">
188
- <ProjectRadar data={analytics.data.projectRadar} />
189
- </ChartErrorBoundary>
190
- </ChartCard>
191
- </div>
201
+ <ChartCard title="Project Comparison">
202
+ <ChartErrorBoundary name="Radar">
203
+ <ProjectRadar data={analytics.data.projectRadar} />
204
+ </ChartErrorBoundary>
205
+ </ChartCard>
192
206
 
193
207
  <ChartCard title="Session Complexity">
194
208
  <ChartErrorBoundary name="Complexity">
@@ -3,14 +3,14 @@ import { useFocusTrap } from "../../hooks/useFocusTrap";
3
3
  import { Maximize2, Minimize2 } from "lucide-react";
4
4
  import type { ReactNode } from "react";
5
5
 
6
- var ChartFullscreenContext = createContext<number | false>(false);
6
+ const ChartFullscreenContext = createContext<number | false>(false);
7
7
 
8
8
  export function useChartFullscreen(): number | false {
9
9
  return useContext(ChartFullscreenContext);
10
10
  }
11
11
 
12
12
  function useViewportChartHeight(): number {
13
- var [h, setH] = useState(Math.round(window.innerHeight * 0.5));
13
+ const [h, setH] = useState(Math.round(window.innerHeight * 0.5));
14
14
  useEffect(function () {
15
15
  function onResize() { setH(Math.round(window.innerHeight * 0.5)); }
16
16
  window.addEventListener("resize", onResize);
@@ -27,13 +27,13 @@ interface ChartCardProps {
27
27
  }
28
28
 
29
29
  export function ChartCard(props: ChartCardProps) {
30
- var [isFullscreen, setIsFullscreen] = useState(false);
31
- var chartHeight = useViewportChartHeight();
32
- var cardRef = useRef<HTMLDivElement>(null);
33
- var [originRect, setOriginRect] = useState<DOMRect | null>(null);
34
- var [animating, setAnimating] = useState(false);
35
- var fullscreenModalRef = useRef<HTMLDivElement>(null);
36
- var closeFullscreenCb = useCallback(function () { closeFullscreen(); }, []);
30
+ const [isFullscreen, setIsFullscreen] = useState(false);
31
+ const chartHeight = useViewportChartHeight();
32
+ const cardRef = useRef<HTMLDivElement>(null);
33
+ const [originRect, setOriginRect] = useState<DOMRect | null>(null);
34
+ const [animating, setAnimating] = useState(false);
35
+ const fullscreenModalRef = useRef<HTMLDivElement>(null);
36
+ const closeFullscreenCb = useCallback(function () { closeFullscreen(); }, []);
37
37
  useFocusTrap(fullscreenModalRef, closeFullscreenCb, isFullscreen);
38
38
 
39
39
  function openFullscreen() {
@@ -67,7 +67,7 @@ export function ChartCard(props: ChartCardProps) {
67
67
  return function () { document.body.style.overflow = ""; };
68
68
  }, [isFullscreen]);
69
69
 
70
- var cardContent = (
70
+ const cardContent = (
71
71
  <>
72
72
  <div className="flex items-center justify-between mb-4">
73
73
  <span className="text-[10px] font-mono font-bold uppercase tracking-widest text-base-content/35">
@@ -91,17 +91,19 @@ export function ChartCard(props: ChartCardProps) {
91
91
  </button>
92
92
  </div>
93
93
  </div>
94
- {props.children}
94
+ <div role="img" aria-label={props.title}>
95
+ {props.children}
96
+ </div>
95
97
  </>
96
98
  );
97
99
 
98
100
  if (isFullscreen) {
99
- var overlayStyle: React.CSSProperties = {
101
+ const overlayStyle: React.CSSProperties = {
100
102
  transition: "opacity 250ms cubic-bezier(0.4, 0, 0.2, 1)",
101
103
  opacity: animating ? 0 : 1,
102
104
  };
103
105
 
104
- var modalStyle: React.CSSProperties = {
106
+ const modalStyle: React.CSSProperties = {
105
107
  transition: "all 250ms cubic-bezier(0.4, 0, 0.2, 1)",
106
108
  };
107
109
 
@@ -118,6 +120,7 @@ export function ChartCard(props: ChartCardProps) {
118
120
  <>
119
121
  <div
120
122
  ref={cardRef}
123
+ aria-label={props.title}
121
124
  className={"rounded-xl border border-base-content/8 bg-base-300/50 p-4 invisible " + (props.className || "")}
122
125
  >
123
126
  {cardContent}
@@ -176,6 +179,7 @@ export function ChartCard(props: ChartCardProps) {
176
179
  return (
177
180
  <div
178
181
  ref={cardRef}
182
+ aria-label={props.title}
179
183
  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 || "")}
180
184
  >
181
185
  {cardContent}
@@ -31,7 +31,7 @@ function Sparkline({ data, stroke }: SparklineProps) {
31
31
  }
32
32
 
33
33
  export function QuickStats() {
34
- var analytics = useAnalytics();
34
+ const analytics = useAnalytics();
35
35
 
36
36
  if (!analytics.data) {
37
37
  return (
@@ -48,22 +48,26 @@ export function QuickStats() {
48
48
  );
49
49
  }
50
50
 
51
- var d = analytics.data;
52
- var colors = getChartColors();
51
+ const d = analytics.data;
52
+ const colors = getChartColors();
53
53
 
54
- var costSparkData = d.costOverTime.slice(-7).map(function (e: typeof d.costOverTime[number]) { return { v: e.total }; });
55
- var sessionsSparkData = d.sessionsOverTime.slice(-7).map(function (e: typeof d.sessionsOverTime[number]) { return { v: e.count }; });
56
- var tokensSparkData = d.tokensOverTime.slice(-7).map(function (e: typeof d.tokensOverTime[number]) { return { v: e.input + e.output }; });
54
+ const costSparkData = d.costOverTime.slice(-7).map(function (e: typeof d.costOverTime[number]) { return { v: e.total }; });
55
+ const sessionsSparkData = d.sessionsOverTime.slice(-7).map(function (e: typeof d.sessionsOverTime[number]) { return { v: e.count }; });
56
+ const tokensSparkData = d.tokensOverTime.slice(-7).map(function (e: typeof d.tokensOverTime[number]) { return { v: e.input + e.output }; });
57
57
 
58
- var totalTokens = d.totalTokens.input + d.totalTokens.output;
59
- var cacheHitPct = Math.round(d.cacheHitRate * 100);
58
+ const totalTokens = d.totalTokens.input + d.totalTokens.output;
59
+ const cacheHitPct = Math.round(d.cacheHitRate * 100);
60
60
 
61
61
  return (
62
62
  <div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
63
63
  <div className="bg-base-content/[0.03] border border-base-content/8 rounded-xl p-3.5">
64
64
  <div className="flex items-center justify-between mb-1">
65
65
  <span className="text-[10px] font-mono font-semibold uppercase tracking-wider text-base-content/35">Cost</span>
66
- {costSparkData.length > 1 && <Sparkline data={costSparkData} stroke={colors.primary} />}
66
+ {costSparkData.length > 1 && (
67
+ <div role="img" aria-label={"Cost trend: " + (costSparkData[costSparkData.length - 1].v > costSparkData[0].v ? "increasing" : costSparkData[costSparkData.length - 1].v < costSparkData[0].v ? "decreasing" : "stable")}>
68
+ <Sparkline data={costSparkData} stroke={colors.primary} />
69
+ </div>
70
+ )}
67
71
  </div>
68
72
  <div className="text-[22px] font-mono text-base-content/85">${d.totalCost.toFixed(2)}</div>
69
73
  </div>
@@ -71,7 +75,11 @@ export function QuickStats() {
71
75
  <div className="bg-base-content/[0.03] border border-base-content/8 rounded-xl p-3.5">
72
76
  <div className="flex items-center justify-between mb-1">
73
77
  <span className="text-[10px] font-mono font-semibold uppercase tracking-wider text-base-content/35">Sessions</span>
74
- {sessionsSparkData.length > 1 && <Sparkline data={sessionsSparkData} stroke={colors.success} />}
78
+ {sessionsSparkData.length > 1 && (
79
+ <div role="img" aria-label={"Sessions trend: " + (sessionsSparkData[sessionsSparkData.length - 1].v > sessionsSparkData[0].v ? "increasing" : sessionsSparkData[sessionsSparkData.length - 1].v < sessionsSparkData[0].v ? "decreasing" : "stable")}>
80
+ <Sparkline data={sessionsSparkData} stroke={colors.success} />
81
+ </div>
82
+ )}
75
83
  </div>
76
84
  <div className="text-[22px] font-mono text-base-content/85">{d.totalSessions}</div>
77
85
  </div>
@@ -79,7 +87,11 @@ export function QuickStats() {
79
87
  <div className="bg-base-content/[0.03] border border-base-content/8 rounded-xl p-3.5">
80
88
  <div className="flex items-center justify-between mb-1">
81
89
  <span className="text-[10px] font-mono font-semibold uppercase tracking-wider text-base-content/35">Tokens</span>
82
- {tokensSparkData.length > 1 && <Sparkline data={tokensSparkData} stroke={colors.warning} />}
90
+ {tokensSparkData.length > 1 && (
91
+ <div role="img" aria-label={"Tokens trend: " + (tokensSparkData[tokensSparkData.length - 1].v > tokensSparkData[0].v ? "increasing" : tokensSparkData[tokensSparkData.length - 1].v < tokensSparkData[0].v ? "decreasing" : "stable")}>
92
+ <Sparkline data={tokensSparkData} stroke={colors.warning} />
93
+ </div>
94
+ )}
83
95
  </div>
84
96
  <div className="text-[22px] font-mono text-base-content/85">{formatTokens(totalTokens)}</div>
85
97
  </div>
@@ -89,7 +101,14 @@ export function QuickStats() {
89
101
  <span className="text-[10px] font-mono font-semibold uppercase tracking-wider text-base-content/35">Cache Hit</span>
90
102
  </div>
91
103
  <div className="text-[22px] font-mono text-base-content/85 mb-2">{cacheHitPct}%</div>
92
- <div className="w-full h-1 rounded-full bg-base-content/10 overflow-hidden">
104
+ <div
105
+ className="w-full h-1 rounded-full bg-base-content/10 overflow-hidden"
106
+ role="progressbar"
107
+ aria-valuenow={cacheHitPct}
108
+ aria-valuemin={0}
109
+ aria-valuemax={100}
110
+ aria-label="Cache hit rate"
111
+ >
93
112
  <div
94
113
  className="h-full rounded-full bg-primary transition-all duration-300"
95
114
  style={{ width: cacheHitPct + "%" }}
@@ -59,7 +59,7 @@ export function ActivityCalendar({ data }: ActivityCalendarProps) {
59
59
  if (!data || data.length === 0) {
60
60
  return (
61
61
  <div className="flex items-center justify-center h-[120px] text-base-content/25 font-mono text-[11px]">
62
- No data
62
+ No data for this period
63
63
  </div>
64
64
  );
65
65
  }
@@ -30,7 +30,7 @@ export function DailySummaryCards({ data }: DailySummaryCardsProps) {
30
30
  if (!data || data.length === 0) {
31
31
  return (
32
32
  <div className="flex items-center justify-center h-[100px] text-base-content/25 font-mono text-[11px]">
33
- No data
33
+ No data for this period
34
34
  </div>
35
35
  );
36
36
  }
@@ -29,7 +29,7 @@ export function HourlyHeatmap({ data }: HourlyHeatmapProps) {
29
29
  if (!data || data.length === 0) {
30
30
  return (
31
31
  <div className="flex items-center justify-center h-[200px] text-base-content/25 font-mono text-[11px]">
32
- No data
32
+ No data for this period
33
33
  </div>
34
34
  );
35
35
  }
@@ -33,7 +33,7 @@ export function SessionTimeline({ data }: SessionTimelineProps) {
33
33
  if (!data || data.length === 0) {
34
34
  return (
35
35
  <div className="flex items-center justify-center h-[200px] text-base-content/25 font-mono text-[11px]">
36
- No data
36
+ No data for this period
37
37
  </div>
38
38
  );
39
39
  }
@@ -10,7 +10,7 @@ import { useSidebar } from "../../hooks/useSidebar";
10
10
  import { useSession } from "../../hooks/useSession";
11
11
  import { clearSession } from "../../stores/session";
12
12
  import { useOnline } from "../../hooks/useOnline";
13
- import { openTab, openSessionTab, getWorkspaceStore } from "../../stores/workspace";
13
+ import { openTab, openSessionTab, closeTab, getWorkspaceStore } from "../../stores/workspace";
14
14
  import { getSidebarStore, goToAnalytics, openSettings } from "../../stores/sidebar";
15
15
  import { setAnalyticsScope } from "../../stores/analytics";
16
16
  import { ProjectRail } from "./ProjectRail";
@@ -269,6 +269,22 @@ export function Sidebar({ onSessionSelect }: { onSessionSelect?: () => void }) {
269
269
  </div>
270
270
  <div className="flex-1 overflow-auto px-4 py-3 pb-16">
271
271
  <div className="flex flex-col gap-0.5 mb-3">
272
+ <button
273
+ type="button"
274
+ onClick={function () {
275
+ var store = getWorkspaceStore();
276
+ var state = store.state;
277
+ var activePane = state.panes.find(function (p) { return p.id === state.activePaneId; });
278
+ var activeTab = activePane ? state.tabs.find(function (t) { return t.id === activePane!.activeTabId; }) : null;
279
+ if (activeTab && activeTab.id !== "chat") {
280
+ closeTab(activeTab.id);
281
+ }
282
+ }}
283
+ className="flex items-center gap-2 w-full px-2 py-1.5 rounded-lg text-[11px] text-base-content/40 hover:text-base-content/70 hover:bg-base-300/30 transition-colors"
284
+ >
285
+ <LayoutDashboard size={12} />
286
+ <span className="font-mono tracking-wide">Dashboard</span>
287
+ </button>
272
288
  <button
273
289
  type="button"
274
290
  onClick={function () {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cryptiklemur/lattice",
3
- "version": "1.22.3",
3
+ "version": "1.23.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>",