@cryptiklemur/lattice 1.4.0 → 1.6.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.
Files changed (31) hide show
  1. package/bun.lock +71 -0
  2. package/client/package.json +1 -0
  3. package/client/src/components/analytics/AnalyticsView.tsx +119 -0
  4. package/client/src/components/analytics/ChartCard.tsx +22 -0
  5. package/client/src/components/analytics/PeriodSelector.tsx +42 -0
  6. package/client/src/components/analytics/QuickStats.tsx +99 -0
  7. package/client/src/components/analytics/charts/CacheEfficiencyChart.tsx +60 -0
  8. package/client/src/components/analytics/charts/ContextUtilizationChart.tsx +110 -0
  9. package/client/src/components/analytics/charts/CostAreaChart.tsx +83 -0
  10. package/client/src/components/analytics/charts/CostDistributionChart.tsx +62 -0
  11. package/client/src/components/analytics/charts/CostDonutChart.tsx +93 -0
  12. package/client/src/components/analytics/charts/CumulativeCostChart.tsx +62 -0
  13. package/client/src/components/analytics/charts/ResponseTimeScatter.tsx +101 -0
  14. package/client/src/components/analytics/charts/SessionBubbleChart.tsx +122 -0
  15. package/client/src/components/analytics/charts/TokenFlowChart.tsx +82 -0
  16. package/client/src/components/analytics/charts/TokenSankeyChart.tsx +89 -0
  17. package/client/src/components/dashboard/DashboardView.tsx +5 -0
  18. package/client/src/components/sidebar/Sidebar.tsx +10 -2
  19. package/client/src/hooks/useAnalytics.ts +75 -0
  20. package/client/src/router.tsx +4 -0
  21. package/client/src/stores/analytics.ts +54 -0
  22. package/client/src/stores/sidebar.ts +8 -0
  23. package/client/vite.config.ts +1 -0
  24. package/package.json +1 -1
  25. package/server/src/analytics/engine.ts +606 -0
  26. package/server/src/daemon.ts +1 -0
  27. package/server/src/handlers/analytics.ts +34 -0
  28. package/server/src/project/session.ts +4 -4
  29. package/shared/src/analytics.ts +28 -0
  30. package/shared/src/index.ts +1 -0
  31. package/shared/src/messages.ts +30 -2
@@ -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
+ }
@@ -4,6 +4,7 @@ import { useProjects } from "../../hooks/useProjects";
4
4
  import { useSidebar } from "../../hooks/useSidebar";
5
5
  import { useWebSocket } from "../../hooks/useWebSocket";
6
6
  import { LatticeLogomark } from "../ui/LatticeLogomark";
7
+ import { QuickStats } from "../analytics/QuickStats";
7
8
  import {
8
9
  Network, FolderOpen, Activity, MessageSquare, Menu,
9
10
  ChevronRight, Lock, Bug,
@@ -118,6 +119,10 @@ export function DashboardView() {
118
119
  </div>
119
120
  </div>
120
121
 
122
+ <div className="mt-4 mb-8">
123
+ <QuickStats />
124
+ </div>
125
+
121
126
  {sessions.length > 0 && (
122
127
  <div className="mb-8">
123
128
  <h2 className="text-[11px] font-semibold tracking-wider uppercase text-base-content/40 mb-3">Recent Sessions</h2>
@@ -1,5 +1,5 @@
1
1
  import { useState, useEffect, useRef } from "react";
2
- import { Plus, ChevronDown, Search, LayoutDashboard, FolderOpen, TerminalSquare, StickyNote, Calendar } from "lucide-react";
2
+ import { Plus, ChevronDown, Search, LayoutDashboard, FolderOpen, TerminalSquare, StickyNote, Calendar, BarChart3 } from "lucide-react";
3
3
  import { LatticeLogomark } from "../ui/LatticeLogomark";
4
4
  import type { SessionSummary, ServerMessage, SettingsDataMessage } from "@lattice/shared";
5
5
  import { useProjects } from "../../hooks/useProjects";
@@ -10,7 +10,7 @@ import { useSession } from "../../hooks/useSession";
10
10
  import { clearSession } from "../../stores/session";
11
11
  import { useOnline } from "../../hooks/useOnline";
12
12
  import { openTab } from "../../stores/workspace";
13
- import { getSidebarStore } from "../../stores/sidebar";
13
+ import { getSidebarStore, goToAnalytics } from "../../stores/sidebar";
14
14
  import { ProjectRail } from "./ProjectRail";
15
15
  import { SessionList } from "./SessionList";
16
16
  import { UserIsland } from "./UserIsland";
@@ -175,6 +175,14 @@ export function Sidebar({ onSessionSelect }: { onSessionSelect?: () => void }) {
175
175
  </button>
176
176
  );
177
177
  })}
178
+ <button
179
+ type="button"
180
+ onClick={goToAnalytics}
181
+ className="flex items-center gap-2 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"
182
+ >
183
+ <BarChart3 size={12} />
184
+ <span className="font-mono tracking-wide">Analytics</span>
185
+ </button>
178
186
  </div>
179
187
 
180
188
  <SectionLabel
@@ -0,0 +1,75 @@
1
+ import { useEffect, useRef } from "react";
2
+ import { useStore } from "@tanstack/react-store";
3
+ import { useWebSocket } from "./useWebSocket";
4
+ import type { ServerMessage } from "@lattice/shared";
5
+ import type { AnalyticsPeriod, AnalyticsScope } from "@lattice/shared";
6
+ import {
7
+ getAnalyticsStore,
8
+ setAnalyticsData,
9
+ setAnalyticsLoading,
10
+ setAnalyticsError,
11
+ setAnalyticsPeriod,
12
+ setAnalyticsScope,
13
+ } from "../stores/analytics";
14
+ import type { AnalyticsState } from "../stores/analytics";
15
+
16
+ export function useAnalytics(): AnalyticsState & {
17
+ setPeriod: (period: AnalyticsPeriod) => void;
18
+ setScope: (scope: AnalyticsScope, projectSlug?: string) => void;
19
+ refresh: () => void;
20
+ } {
21
+ var store = getAnalyticsStore();
22
+ var state = useStore(store, function (s) { return s; });
23
+ var { send, subscribe, unsubscribe } = useWebSocket();
24
+ var sendRef = useRef(send);
25
+ sendRef.current = send;
26
+
27
+ function requestAnalytics(forceRefresh?: boolean) {
28
+ var s = getAnalyticsStore().state;
29
+ setAnalyticsLoading(true);
30
+ sendRef.current({
31
+ type: "analytics:request",
32
+ requestId: crypto.randomUUID(),
33
+ scope: s.scope,
34
+ projectSlug: s.projectSlug || undefined,
35
+ period: s.period,
36
+ forceRefresh: forceRefresh,
37
+ } as any);
38
+ }
39
+
40
+ useEffect(function () {
41
+ function handleData(msg: ServerMessage) {
42
+ var m = msg as { type: string; data: any };
43
+ setAnalyticsData(m.data);
44
+ }
45
+
46
+ function handleError(msg: ServerMessage) {
47
+ var m = msg as { type: string; message: string };
48
+ setAnalyticsError(m.message);
49
+ }
50
+
51
+ subscribe("analytics:data", handleData);
52
+ subscribe("analytics:error", handleError);
53
+
54
+ return function () {
55
+ unsubscribe("analytics:data", handleData);
56
+ unsubscribe("analytics:error", handleError);
57
+ };
58
+ }, [subscribe, unsubscribe]);
59
+
60
+ useEffect(function () {
61
+ requestAnalytics();
62
+ }, [state.period, state.scope, state.projectSlug]);
63
+
64
+ return {
65
+ data: state.data,
66
+ loading: state.loading,
67
+ error: state.error,
68
+ period: state.period,
69
+ scope: state.scope,
70
+ projectSlug: state.projectSlug,
71
+ setPeriod: setAnalyticsPeriod,
72
+ setScope: setAnalyticsScope,
73
+ refresh: function () { requestAnalytics(true); },
74
+ };
75
+ }
@@ -8,6 +8,7 @@ import { SettingsView } from "./components/settings/SettingsView";
8
8
  import { ProjectSettingsView } from "./components/project-settings/ProjectSettingsView";
9
9
  import { DashboardView } from "./components/dashboard/DashboardView";
10
10
  import { ProjectDashboardView } from "./components/dashboard/ProjectDashboardView";
11
+ import { AnalyticsView } from "./components/analytics/AnalyticsView";
11
12
  import { NodeSettingsModal } from "./components/sidebar/NodeSettingsModal";
12
13
  import { AddProjectModal } from "./components/sidebar/AddProjectModal";
13
14
  import { useSidebar } from "./hooks/useSidebar";
@@ -421,6 +422,9 @@ function IndexPage() {
421
422
  if (sidebar.activeView.type === "project-dashboard") {
422
423
  return <ProjectDashboardView />;
423
424
  }
425
+ if (sidebar.activeView.type === "analytics") {
426
+ return <AnalyticsView />;
427
+ }
424
428
  return <WorkspaceView />;
425
429
  }
426
430
 
@@ -0,0 +1,54 @@
1
+ import { Store } from "@tanstack/react-store";
2
+ import type { AnalyticsPayload, AnalyticsPeriod, AnalyticsScope } from "@lattice/shared";
3
+
4
+ export interface AnalyticsState {
5
+ data: AnalyticsPayload | null;
6
+ loading: boolean;
7
+ error: string | null;
8
+ period: AnalyticsPeriod;
9
+ scope: AnalyticsScope;
10
+ projectSlug: string | null;
11
+ }
12
+
13
+ var analyticsStore = new Store<AnalyticsState>({
14
+ data: null,
15
+ loading: false,
16
+ error: null,
17
+ period: "7d",
18
+ scope: "global",
19
+ projectSlug: null,
20
+ });
21
+
22
+ export function getAnalyticsStore(): Store<AnalyticsState> {
23
+ return analyticsStore;
24
+ }
25
+
26
+ export function setAnalyticsData(data: AnalyticsPayload): void {
27
+ analyticsStore.setState(function (state) {
28
+ return { ...state, data: data, loading: false, error: null };
29
+ });
30
+ }
31
+
32
+ export function setAnalyticsLoading(loading: boolean): void {
33
+ analyticsStore.setState(function (state) {
34
+ return { ...state, loading: loading };
35
+ });
36
+ }
37
+
38
+ export function setAnalyticsError(error: string): void {
39
+ analyticsStore.setState(function (state) {
40
+ return { ...state, error: error, loading: false };
41
+ });
42
+ }
43
+
44
+ export function setAnalyticsPeriod(period: AnalyticsPeriod): void {
45
+ analyticsStore.setState(function (state) {
46
+ return { ...state, period: period };
47
+ });
48
+ }
49
+
50
+ export function setAnalyticsScope(scope: AnalyticsScope, projectSlug?: string): void {
51
+ analyticsStore.setState(function (state) {
52
+ return { ...state, scope: scope, projectSlug: projectSlug || null };
53
+ });
54
+ }
@@ -13,6 +13,7 @@ export type SidebarMode = "project" | "settings";
13
13
  export type ActiveView =
14
14
  | { type: "dashboard" }
15
15
  | { type: "project-dashboard" }
16
+ | { type: "analytics" }
16
17
  | { type: "chat" }
17
18
  | { type: "settings"; section: SettingsSection }
18
19
  | { type: "project-settings"; section: ProjectSettingsSection };
@@ -246,6 +247,13 @@ export function goToDashboard(): void {
246
247
  pushUrl(null, null);
247
248
  }
248
249
 
250
+ export function goToAnalytics(): void {
251
+ sidebarStore.setState(function (state) {
252
+ return { ...state, activeView: { type: "analytics" }, sidebarMode: "project" };
253
+ });
254
+ pushUrl(sidebarStore.state.activeProjectSlug, null);
255
+ }
256
+
249
257
  export function handlePopState(): void {
250
258
  var url = parseInitialUrl();
251
259
  if (url.settingsSection) {
@@ -10,6 +10,7 @@ export default defineConfig({
10
10
  VitePWA({
11
11
  registerType: "prompt",
12
12
  workbox: {
13
+ maximumFileSizeToCacheInBytes: 4 * 1024 * 1024,
13
14
  globPatterns: ["**/*.{js,css,html,svg,png,woff2}"],
14
15
  navigateFallback: "/index.html",
15
16
  navigateFallbackDenylist: [/^\/ws/, /^\/api/],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cryptiklemur/lattice",
3
- "version": "1.4.0",
3
+ "version": "1.6.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>",