@cryptiklemur/lattice 1.4.0 → 1.5.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/bun.lock +71 -0
- package/client/package.json +1 -0
- package/client/src/components/analytics/AnalyticsView.tsx +61 -0
- package/client/src/components/analytics/ChartCard.tsx +22 -0
- package/client/src/components/analytics/PeriodSelector.tsx +42 -0
- package/client/src/components/analytics/QuickStats.tsx +99 -0
- package/client/src/components/analytics/charts/CostAreaChart.tsx +83 -0
- package/client/src/components/analytics/charts/CostDistributionChart.tsx +62 -0
- package/client/src/components/analytics/charts/CostDonutChart.tsx +93 -0
- package/client/src/components/analytics/charts/CumulativeCostChart.tsx +62 -0
- package/client/src/components/analytics/charts/SessionBubbleChart.tsx +122 -0
- package/client/src/components/dashboard/DashboardView.tsx +5 -0
- package/client/src/components/sidebar/Sidebar.tsx +10 -2
- package/client/src/hooks/useAnalytics.ts +75 -0
- package/client/src/router.tsx +4 -0
- package/client/src/stores/analytics.ts +54 -0
- package/client/src/stores/sidebar.ts +8 -0
- package/client/vite.config.ts +1 -0
- package/package.json +1 -1
- package/server/src/analytics/engine.ts +491 -0
- package/server/src/daemon.ts +1 -0
- package/server/src/handlers/analytics.ts +34 -0
- package/server/src/project/session.ts +4 -4
- package/shared/src/analytics.ts +24 -0
- package/shared/src/index.ts +1 -0
- package/shared/src/messages.ts +30 -2
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { PieChart, Pie, Cell, Tooltip, ResponsiveContainer } from "recharts";
|
|
2
|
+
|
|
3
|
+
var MODEL_COLORS: Record<string, string> = {
|
|
4
|
+
opus: "#a855f7",
|
|
5
|
+
sonnet: "oklch(55% 0.25 280)",
|
|
6
|
+
haiku: "#22c55e",
|
|
7
|
+
other: "#f59e0b",
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
function getModelColor(model: string): string {
|
|
11
|
+
var key = model.toLowerCase();
|
|
12
|
+
if (key.includes("opus")) return MODEL_COLORS.opus;
|
|
13
|
+
if (key.includes("sonnet")) return MODEL_COLORS.sonnet;
|
|
14
|
+
if (key.includes("haiku")) return MODEL_COLORS.haiku;
|
|
15
|
+
return MODEL_COLORS.other;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface ModelUsage {
|
|
19
|
+
model: string;
|
|
20
|
+
cost: number;
|
|
21
|
+
percentage: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface CostDonutChartProps {
|
|
25
|
+
modelUsage: ModelUsage[];
|
|
26
|
+
totalCost: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function CustomTooltip({ active, payload }: { active?: boolean; payload?: Array<{ name: string; value: number; payload: ModelUsage }> }) {
|
|
30
|
+
if (!active || !payload || payload.length === 0) return null;
|
|
31
|
+
var entry = payload[0];
|
|
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">{entry.name}</p>
|
|
35
|
+
<p className="text-[11px] font-mono text-base-content">${entry.value.toFixed(4)}</p>
|
|
36
|
+
<p className="text-[10px] font-mono text-base-content/50">{entry.payload.percentage.toFixed(1)}%</p>
|
|
37
|
+
</div>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function CenterLabel({ totalCost }: { totalCost: number }) {
|
|
42
|
+
return (
|
|
43
|
+
<text x="50%" y="50%" textAnchor="middle" dominantBaseline="middle">
|
|
44
|
+
<tspan x="50%" dy="-0.4em" style={{ fontSize: 11, fontFamily: "var(--font-mono)", fill: "oklch(0.9 0.02 280 / 0.4)" }}>
|
|
45
|
+
TOTAL
|
|
46
|
+
</tspan>
|
|
47
|
+
<tspan x="50%" dy="1.4em" style={{ fontSize: 14, fontFamily: "var(--font-mono)", fill: "oklch(0.9 0.02 280 / 0.9)", fontWeight: 700 }}>
|
|
48
|
+
${totalCost.toFixed(2)}
|
|
49
|
+
</tspan>
|
|
50
|
+
</text>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function CostDonutChart({ modelUsage, totalCost }: CostDonutChartProps) {
|
|
55
|
+
return (
|
|
56
|
+
<div>
|
|
57
|
+
<ResponsiveContainer width="100%" height={200}>
|
|
58
|
+
<PieChart>
|
|
59
|
+
<Pie
|
|
60
|
+
data={modelUsage}
|
|
61
|
+
dataKey="cost"
|
|
62
|
+
nameKey="model"
|
|
63
|
+
innerRadius={50}
|
|
64
|
+
outerRadius={80}
|
|
65
|
+
paddingAngle={2}
|
|
66
|
+
startAngle={90}
|
|
67
|
+
endAngle={-270}
|
|
68
|
+
>
|
|
69
|
+
{modelUsage.map(function (entry, index) {
|
|
70
|
+
return <Cell key={entry.model + index} fill={getModelColor(entry.model)} />;
|
|
71
|
+
})}
|
|
72
|
+
</Pie>
|
|
73
|
+
<Tooltip content={<CustomTooltip />} />
|
|
74
|
+
<CenterLabel totalCost={totalCost} />
|
|
75
|
+
</PieChart>
|
|
76
|
+
</ResponsiveContainer>
|
|
77
|
+
<div className="flex flex-wrap justify-center gap-3 mt-2">
|
|
78
|
+
{modelUsage.map(function (entry) {
|
|
79
|
+
return (
|
|
80
|
+
<div key={entry.model} className="flex items-center gap-1.5 text-[10px] font-mono text-base-content/50">
|
|
81
|
+
<span
|
|
82
|
+
className="inline-block w-2 h-2 rounded-full flex-shrink-0"
|
|
83
|
+
style={{ background: getModelColor(entry.model) }}
|
|
84
|
+
/>
|
|
85
|
+
<span className="capitalize">{entry.model}</span>
|
|
86
|
+
<span className="text-base-content/30">{entry.percentage.toFixed(1)}%</span>
|
|
87
|
+
</div>
|
|
88
|
+
);
|
|
89
|
+
})}
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
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 CumulativeDatum {
|
|
20
|
+
date: string;
|
|
21
|
+
total: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface CumulativeCostChartProps {
|
|
25
|
+
data: CumulativeDatum[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function CustomTooltip({ active, payload, label }: { active?: boolean; payload?: Array<{ value: number }>; label?: string }) {
|
|
29
|
+
if (!active || !payload || payload.length === 0) return null;
|
|
30
|
+
return (
|
|
31
|
+
<div className="rounded-lg border border-base-content/8 bg-base-200 px-3 py-2 shadow-lg">
|
|
32
|
+
<p className="text-[10px] font-mono text-base-content/50 mb-1">{label}</p>
|
|
33
|
+
<p className="text-[11px] font-mono text-base-content">${payload[0].value.toFixed(4)}</p>
|
|
34
|
+
</div>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function CumulativeCostChart({ data }: CumulativeCostChartProps) {
|
|
39
|
+
return (
|
|
40
|
+
<ResponsiveContainer width="100%" height={200}>
|
|
41
|
+
<AreaChart data={data} margin={{ top: 4, right: 4, left: -20, bottom: 0 }}>
|
|
42
|
+
<defs>
|
|
43
|
+
<linearGradient id="cumulativeGrad" x1="0" y1="0" x2="0" y2="1">
|
|
44
|
+
<stop offset="5%" stopColor="oklch(55% 0.25 280)" stopOpacity={0.3} />
|
|
45
|
+
<stop offset="95%" stopColor="oklch(55% 0.25 280)" stopOpacity={0} />
|
|
46
|
+
</linearGradient>
|
|
47
|
+
</defs>
|
|
48
|
+
<CartesianGrid strokeDasharray="3 3" stroke={GRID_COLOR} vertical={false} />
|
|
49
|
+
<XAxis dataKey="date" tick={TICK_STYLE} axisLine={false} tickLine={false} />
|
|
50
|
+
<YAxis tick={TICK_STYLE} axisLine={false} tickLine={false} tickFormatter={function (v) { return "$" + v.toFixed(2); }} />
|
|
51
|
+
<Tooltip content={<CustomTooltip />} />
|
|
52
|
+
<Area
|
|
53
|
+
type="monotone"
|
|
54
|
+
dataKey="total"
|
|
55
|
+
stroke="oklch(55% 0.25 280)"
|
|
56
|
+
fill="url(#cumulativeGrad)"
|
|
57
|
+
strokeWidth={2}
|
|
58
|
+
/>
|
|
59
|
+
</AreaChart>
|
|
60
|
+
</ResponsiveContainer>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ScatterChart,
|
|
3
|
+
Scatter,
|
|
4
|
+
XAxis,
|
|
5
|
+
YAxis,
|
|
6
|
+
CartesianGrid,
|
|
7
|
+
Tooltip,
|
|
8
|
+
ResponsiveContainer,
|
|
9
|
+
ZAxis,
|
|
10
|
+
} from "recharts";
|
|
11
|
+
|
|
12
|
+
var TICK_STYLE = {
|
|
13
|
+
fontSize: 10,
|
|
14
|
+
fontFamily: "var(--font-mono)",
|
|
15
|
+
fill: "oklch(0.9 0.02 280 / 0.3)",
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
var GRID_COLOR = "oklch(0.9 0.02 280 / 0.06)";
|
|
19
|
+
|
|
20
|
+
var PROJECT_PALETTE = [
|
|
21
|
+
"oklch(55% 0.25 280)",
|
|
22
|
+
"#a855f7",
|
|
23
|
+
"#22c55e",
|
|
24
|
+
"#f59e0b",
|
|
25
|
+
"oklch(65% 0.2 240)",
|
|
26
|
+
"oklch(65% 0.25 25)",
|
|
27
|
+
"oklch(65% 0.25 150)",
|
|
28
|
+
"oklch(70% 0.2 60)",
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
interface SessionBubbleDatum {
|
|
32
|
+
id: string;
|
|
33
|
+
title: string;
|
|
34
|
+
cost: number;
|
|
35
|
+
tokens: number;
|
|
36
|
+
timestamp: number;
|
|
37
|
+
project: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface SessionBubbleChartProps {
|
|
41
|
+
data: SessionBubbleDatum[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function formatDate(ts: number): string {
|
|
45
|
+
var d = new Date(ts);
|
|
46
|
+
return (d.getMonth() + 1) + "/" + d.getDate();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function CustomTooltip({ active, payload }: { active?: boolean; payload?: Array<{ payload: SessionBubbleDatum }> }) {
|
|
50
|
+
if (!active || !payload || payload.length === 0) return null;
|
|
51
|
+
var d = payload[0].payload;
|
|
52
|
+
return (
|
|
53
|
+
<div className="rounded-lg border border-base-content/8 bg-base-200 px-3 py-2 shadow-lg max-w-[180px]">
|
|
54
|
+
<p className="text-[10px] font-mono text-base-content/50 mb-1 truncate">{d.title || d.id}</p>
|
|
55
|
+
<div className="text-[11px] font-mono text-base-content/70 space-y-0.5">
|
|
56
|
+
<p><span className="text-base-content/40">cost </span>${d.cost.toFixed(4)}</p>
|
|
57
|
+
<p><span className="text-base-content/40">tokens </span>{d.tokens.toLocaleString()}</p>
|
|
58
|
+
<p><span className="text-base-content/40">project </span>{d.project}</p>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function SessionBubbleChart({ data }: SessionBubbleChartProps) {
|
|
65
|
+
var projects = Array.from(new Set(data.map(function (d) { return d.project; })));
|
|
66
|
+
|
|
67
|
+
function getColor(project: string): string {
|
|
68
|
+
var idx = projects.indexOf(project);
|
|
69
|
+
return PROJECT_PALETTE[idx % PROJECT_PALETTE.length];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
var byProject = projects.map(function (project) {
|
|
73
|
+
return {
|
|
74
|
+
project,
|
|
75
|
+
color: getColor(project),
|
|
76
|
+
points: data
|
|
77
|
+
.filter(function (d) { return d.project === project; })
|
|
78
|
+
.map(function (d) { return { ...d, x: d.timestamp, y: d.tokens, z: Math.max(d.cost * 1000, 20) }; }),
|
|
79
|
+
};
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
var minTs = Math.min(...data.map(function (d) { return d.timestamp; }));
|
|
83
|
+
var maxTs = Math.max(...data.map(function (d) { return d.timestamp; }));
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<ResponsiveContainer width="100%" height={200}>
|
|
87
|
+
<ScatterChart margin={{ top: 4, right: 4, left: -20, bottom: 0 }}>
|
|
88
|
+
<CartesianGrid strokeDasharray="3 3" stroke={GRID_COLOR} />
|
|
89
|
+
<XAxis
|
|
90
|
+
dataKey="x"
|
|
91
|
+
type="number"
|
|
92
|
+
domain={[minTs, maxTs]}
|
|
93
|
+
tick={TICK_STYLE}
|
|
94
|
+
axisLine={false}
|
|
95
|
+
tickLine={false}
|
|
96
|
+
tickFormatter={function (v) { return formatDate(v); }}
|
|
97
|
+
/>
|
|
98
|
+
<YAxis
|
|
99
|
+
dataKey="y"
|
|
100
|
+
type="number"
|
|
101
|
+
tick={TICK_STYLE}
|
|
102
|
+
axisLine={false}
|
|
103
|
+
tickLine={false}
|
|
104
|
+
tickFormatter={function (v) { return v >= 1000 ? (v / 1000).toFixed(0) + "k" : String(v); }}
|
|
105
|
+
/>
|
|
106
|
+
<ZAxis dataKey="z" range={[20, 300]} />
|
|
107
|
+
<Tooltip content={<CustomTooltip />} cursor={{ strokeDasharray: "3 3", stroke: GRID_COLOR }} />
|
|
108
|
+
{byProject.map(function (group) {
|
|
109
|
+
return (
|
|
110
|
+
<Scatter
|
|
111
|
+
key={group.project}
|
|
112
|
+
name={group.project}
|
|
113
|
+
data={group.points}
|
|
114
|
+
fill={group.color}
|
|
115
|
+
fillOpacity={0.7}
|
|
116
|
+
/>
|
|
117
|
+
);
|
|
118
|
+
})}
|
|
119
|
+
</ScatterChart>
|
|
120
|
+
</ResponsiveContainer>
|
|
121
|
+
);
|
|
122
|
+
}
|
|
@@ -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
|
+
}
|
package/client/src/router.tsx
CHANGED
|
@@ -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) {
|
package/client/vite.config.ts
CHANGED
|
@@ -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.
|
|
3
|
+
"version": "1.5.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>",
|