@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.
- package/bun.lock +71 -0
- package/client/package.json +1 -0
- package/client/src/components/analytics/AnalyticsView.tsx +119 -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/CacheEfficiencyChart.tsx +60 -0
- package/client/src/components/analytics/charts/ContextUtilizationChart.tsx +110 -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/ResponseTimeScatter.tsx +101 -0
- package/client/src/components/analytics/charts/SessionBubbleChart.tsx +122 -0
- package/client/src/components/analytics/charts/TokenFlowChart.tsx +82 -0
- package/client/src/components/analytics/charts/TokenSankeyChart.tsx +89 -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 +606 -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 +28 -0
- package/shared/src/index.ts +1 -0
- 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
|
+
}
|
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.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>",
|