@cryptiklemur/lattice 1.18.1 → 1.18.3
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/client/src/components/analytics/AnalyticsView.tsx +1 -22
- package/client/src/components/analytics/charts/ActivityCalendar.tsx +21 -7
- package/client/src/components/project-settings/ProjectClaude.tsx +1 -1
- package/client/src/components/project-settings/ProjectEnvironment.tsx +1 -1
- package/client/src/components/project-settings/ProjectGeneral.tsx +1 -1
- package/client/src/components/project-settings/ProjectMcp.tsx +1 -1
- package/client/src/components/project-settings/ProjectPermissions.tsx +1 -1
- package/client/src/components/project-settings/ProjectRules.tsx +1 -1
- package/client/src/components/settings/BudgetSettings.tsx +1 -1
- package/client/src/components/settings/ClaudeSettings.tsx +1 -1
- package/client/src/components/settings/Editor.tsx +1 -1
- package/client/src/components/settings/Environment.tsx +2 -2
- package/client/src/components/settings/GlobalMcp.tsx +1 -1
- package/client/src/hooks/useSaveState.ts +7 -1
- package/package.json +1 -1
- package/server/src/analytics/engine.ts +10 -6
|
@@ -50,9 +50,6 @@ import { ActivityCalendar } from "./charts/ActivityCalendar";
|
|
|
50
50
|
import { HourlyHeatmap } from "./charts/HourlyHeatmap";
|
|
51
51
|
import { SessionTimeline } from "./charts/SessionTimeline";
|
|
52
52
|
import { DailySummaryCards } from "./charts/DailySummaryCards";
|
|
53
|
-
import { ToolTreemap } from "./charts/ToolTreemap";
|
|
54
|
-
import { ToolSunburst } from "./charts/ToolSunburst";
|
|
55
|
-
import { PermissionBreakdown } from "./charts/PermissionBreakdown";
|
|
56
53
|
import { ProjectRadar } from "./charts/ProjectRadar";
|
|
57
54
|
import { SessionComplexityList } from "./charts/SessionComplexityList";
|
|
58
55
|
import { NodeFleetOverview } from "./charts/NodeFleetOverview";
|
|
@@ -183,27 +180,9 @@ export function AnalyticsView() {
|
|
|
183
180
|
</ChartErrorBoundary>
|
|
184
181
|
</ChartCard>
|
|
185
182
|
|
|
186
|
-
<SectionHeader label="
|
|
183
|
+
<SectionHeader label="Projects" />
|
|
187
184
|
|
|
188
185
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
189
|
-
<ChartCard title="Tool Usage (Treemap)">
|
|
190
|
-
<ChartErrorBoundary name="Treemap">
|
|
191
|
-
<ToolTreemap data={analytics.data.toolTreemap} />
|
|
192
|
-
</ChartErrorBoundary>
|
|
193
|
-
</ChartCard>
|
|
194
|
-
<ChartCard title="Tool Categories">
|
|
195
|
-
<ChartErrorBoundary name="Sunburst">
|
|
196
|
-
<ToolSunburst data={analytics.data.toolSunburst} />
|
|
197
|
-
</ChartErrorBoundary>
|
|
198
|
-
</ChartCard>
|
|
199
|
-
</div>
|
|
200
|
-
|
|
201
|
-
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
202
|
-
<ChartCard title="Permissions">
|
|
203
|
-
<ChartErrorBoundary name="Permissions">
|
|
204
|
-
<PermissionBreakdown data={analytics.data.permissionStats} />
|
|
205
|
-
</ChartErrorBoundary>
|
|
206
|
-
</ChartCard>
|
|
207
186
|
<ChartCard title="Project Comparison">
|
|
208
187
|
<ChartErrorBoundary name="Radar">
|
|
209
188
|
<ProjectRadar data={analytics.data.projectRadar} />
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState } from "react";
|
|
1
|
+
import { useState, useRef, useEffect } from "react";
|
|
2
2
|
import { getChartColors } from "../chartTokens";
|
|
3
3
|
|
|
4
4
|
interface CalendarDatum {
|
|
@@ -12,11 +12,8 @@ interface ActivityCalendarProps {
|
|
|
12
12
|
data: CalendarDatum[];
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
var CELL_SIZE = 11;
|
|
16
|
-
var CELL_GAP = 2;
|
|
17
|
-
var CELL_STEP = CELL_SIZE + CELL_GAP;
|
|
18
15
|
var DAY_LABEL_WIDTH = 28;
|
|
19
|
-
var MONTH_LABEL_HEIGHT =
|
|
16
|
+
var MONTH_LABEL_HEIGHT = 16;
|
|
20
17
|
|
|
21
18
|
var INTENSITY_OPACITIES = [0.05, 0.15, 0.3, 0.5, 0.8];
|
|
22
19
|
|
|
@@ -46,6 +43,18 @@ export function ActivityCalendar({ data }: ActivityCalendarProps) {
|
|
|
46
43
|
var colors = getChartColors();
|
|
47
44
|
var PRIMARY_COLOR = colors.primary;
|
|
48
45
|
var [hover, setHover] = useState<{ x: number; y: number; datum: CalendarDatum } | null>(null);
|
|
46
|
+
var containerRef = useRef<HTMLDivElement>(null);
|
|
47
|
+
var [containerWidth, setContainerWidth] = useState(0);
|
|
48
|
+
|
|
49
|
+
useEffect(function () {
|
|
50
|
+
if (!containerRef.current) return;
|
|
51
|
+
let ro = new ResizeObserver(function (entries) {
|
|
52
|
+
setContainerWidth(entries[0].contentRect.width);
|
|
53
|
+
});
|
|
54
|
+
ro.observe(containerRef.current);
|
|
55
|
+
setContainerWidth(containerRef.current.clientWidth);
|
|
56
|
+
return function () { ro.disconnect(); };
|
|
57
|
+
}, []);
|
|
49
58
|
|
|
50
59
|
if (!data || data.length === 0) {
|
|
51
60
|
return (
|
|
@@ -101,12 +110,17 @@ export function ActivityCalendar({ data }: ActivityCalendarProps) {
|
|
|
101
110
|
}
|
|
102
111
|
}
|
|
103
112
|
|
|
113
|
+
var availableWidth = (containerWidth || 800) - DAY_LABEL_WIDTH;
|
|
114
|
+
var CELL_STEP = Math.max(3, Math.floor(availableWidth / weeks.length));
|
|
115
|
+
var CELL_GAP = Math.max(1, Math.round(CELL_STEP * 0.15));
|
|
116
|
+
var CELL_SIZE = CELL_STEP - CELL_GAP;
|
|
117
|
+
|
|
104
118
|
var svgWidth = DAY_LABEL_WIDTH + weeks.length * CELL_STEP;
|
|
105
119
|
var svgHeight = MONTH_LABEL_HEIGHT + 7 * CELL_STEP;
|
|
106
120
|
|
|
107
121
|
return (
|
|
108
|
-
<div className="relative
|
|
109
|
-
<svg width={
|
|
122
|
+
<div ref={containerRef} className="relative w-full">
|
|
123
|
+
<svg width="100%" height={svgHeight} viewBox={"0 0 " + svgWidth + " " + svgHeight} className="block">
|
|
110
124
|
{monthLabels.map(function (ml, idx) {
|
|
111
125
|
return (
|
|
112
126
|
<text
|
|
@@ -62,7 +62,7 @@ export function ProjectClaude({ settings, updateSection }: ProjectClaudeProps) {
|
|
|
62
62
|
var save = useSaveState();
|
|
63
63
|
|
|
64
64
|
useEffect(function () {
|
|
65
|
-
if (save.
|
|
65
|
+
if (save.savingRef.current) {
|
|
66
66
|
save.confirmSave();
|
|
67
67
|
} else {
|
|
68
68
|
setClaudeMd(settings.claudeMd ?? "");
|
|
@@ -35,7 +35,7 @@ export function ProjectMcp({
|
|
|
35
35
|
var [confirmDelete, setConfirmDelete] = useState<string | null>(null);
|
|
36
36
|
|
|
37
37
|
useEffect(function () {
|
|
38
|
-
if (save.
|
|
38
|
+
if (save.savingRef.current) {
|
|
39
39
|
save.confirmSave();
|
|
40
40
|
} else {
|
|
41
41
|
setServers({ ...(settings.mcpServers ?? {}) });
|
|
@@ -31,7 +31,7 @@ export function ProjectRules({
|
|
|
31
31
|
var save = useSaveState();
|
|
32
32
|
|
|
33
33
|
useEffect(function () {
|
|
34
|
-
if (save.
|
|
34
|
+
if (save.savingRef.current) {
|
|
35
35
|
save.confirmSave();
|
|
36
36
|
} else {
|
|
37
37
|
setRules((settings.rules ?? []).map(function (r: { filename: string; content: string }) {
|
|
@@ -39,7 +39,7 @@ export function ClaudeSettings() {
|
|
|
39
39
|
var newModel = cfg.defaultModel ? String(cfg.defaultModel) : CLAUDE_MODELS[0].id;
|
|
40
40
|
var newEffort = cfg.defaultEffort ? String(cfg.defaultEffort) : "normal";
|
|
41
41
|
|
|
42
|
-
if (save.
|
|
42
|
+
if (save.savingRef.current) {
|
|
43
43
|
save.confirmSave();
|
|
44
44
|
} else {
|
|
45
45
|
setClaudeMd(newClaudeMd);
|
|
@@ -35,7 +35,7 @@ export function Editor() {
|
|
|
35
35
|
var newType = editor?.type ?? "vscode";
|
|
36
36
|
var newCustomCommand = editor?.customCommand ?? "";
|
|
37
37
|
|
|
38
|
-
if (saveRef.current.
|
|
38
|
+
if (saveRef.current.savingRef.current) {
|
|
39
39
|
saveRef.current.confirmSave();
|
|
40
40
|
} else {
|
|
41
41
|
setIdeType(newType);
|
|
@@ -29,7 +29,7 @@ export function Environment() {
|
|
|
29
29
|
var data = msg as SettingsDataMessage;
|
|
30
30
|
var env = data.config.globalEnv ?? {};
|
|
31
31
|
|
|
32
|
-
if (save.
|
|
32
|
+
if (save.savingRef.current) {
|
|
33
33
|
save.confirmSave();
|
|
34
34
|
}
|
|
35
35
|
|
|
@@ -37,7 +37,7 @@ export function Environment() {
|
|
|
37
37
|
return { id: genId(), key: k, value: String(v) };
|
|
38
38
|
});
|
|
39
39
|
setEntries(rows);
|
|
40
|
-
if (!save.
|
|
40
|
+
if (!save.savingRef.current) {
|
|
41
41
|
save.resetFromServer();
|
|
42
42
|
}
|
|
43
43
|
}
|
|
@@ -5,6 +5,7 @@ export type SaveState = "idle" | "saved" | "error";
|
|
|
5
5
|
export interface UseSaveStateReturn {
|
|
6
6
|
dirty: boolean;
|
|
7
7
|
saving: boolean;
|
|
8
|
+
savingRef: React.RefObject<boolean>;
|
|
8
9
|
saveState: SaveState;
|
|
9
10
|
markDirty: () => void;
|
|
10
11
|
startSave: () => void;
|
|
@@ -17,6 +18,7 @@ export function useSaveState(): UseSaveStateReturn {
|
|
|
17
18
|
var [saving, setSaving] = useState(false);
|
|
18
19
|
var [saveState, setSaveState] = useState<SaveState>("idle");
|
|
19
20
|
var saveTimeout = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
21
|
+
var savingRef = useRef(false);
|
|
20
22
|
|
|
21
23
|
useEffect(function () {
|
|
22
24
|
return function () {
|
|
@@ -31,11 +33,13 @@ export function useSaveState(): UseSaveStateReturn {
|
|
|
31
33
|
|
|
32
34
|
function startSave() {
|
|
33
35
|
setSaving(true);
|
|
36
|
+
savingRef.current = true;
|
|
34
37
|
setSaveState("idle");
|
|
35
38
|
|
|
36
39
|
if (saveTimeout.current) clearTimeout(saveTimeout.current);
|
|
37
40
|
saveTimeout.current = setTimeout(function () {
|
|
38
41
|
setSaving(false);
|
|
42
|
+
savingRef.current = false;
|
|
39
43
|
setSaveState("error");
|
|
40
44
|
setTimeout(function () { setSaveState("idle"); }, 3000);
|
|
41
45
|
}, 5000);
|
|
@@ -43,6 +47,7 @@ export function useSaveState(): UseSaveStateReturn {
|
|
|
43
47
|
|
|
44
48
|
function confirmSave() {
|
|
45
49
|
setSaving(false);
|
|
50
|
+
savingRef.current = false;
|
|
46
51
|
setSaveState("saved");
|
|
47
52
|
setDirty(false);
|
|
48
53
|
if (saveTimeout.current) clearTimeout(saveTimeout.current);
|
|
@@ -53,7 +58,8 @@ export function useSaveState(): UseSaveStateReturn {
|
|
|
53
58
|
setDirty(false);
|
|
54
59
|
setSaveState("idle");
|
|
55
60
|
setSaving(false);
|
|
61
|
+
savingRef.current = false;
|
|
56
62
|
}
|
|
57
63
|
|
|
58
|
-
return { dirty, saving, saveState, markDirty, startSave, confirmSave, resetFromServer };
|
|
64
|
+
return { dirty, saving, savingRef, saveState, markDirty, startSave, confirmSave, resetFromServer };
|
|
59
65
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cryptiklemur/lattice",
|
|
3
|
-
"version": "1.18.
|
|
3
|
+
"version": "1.18.3",
|
|
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>",
|
|
@@ -64,8 +64,9 @@ function formatDate(ts: number): string {
|
|
|
64
64
|
return year + "-" + month + "-" + day;
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
function getCostBucket(cost: number): string {
|
|
68
|
-
if (cost
|
|
67
|
+
function getCostBucket(cost: number): string | null {
|
|
68
|
+
if (cost <= 0) return null;
|
|
69
|
+
if (cost < 0.01) return "<$0.01";
|
|
69
70
|
if (cost < 0.05) return "$0.01-0.05";
|
|
70
71
|
if (cost < 0.10) return "$0.05-0.10";
|
|
71
72
|
if (cost < 0.50) return "$0.10-0.50";
|
|
@@ -254,7 +255,7 @@ function aggregate(sessions: SessionData[], period: AnalyticsPeriod): AnalyticsP
|
|
|
254
255
|
var toolStats = new Map<string, { count: number; totalCost: number; sessions: number }>();
|
|
255
256
|
|
|
256
257
|
var costBuckets = new Map<string, number>();
|
|
257
|
-
var bucketOrder = ["
|
|
258
|
+
var bucketOrder = ["<$0.01", "$0.01-0.05", "$0.05-0.10", "$0.10-0.50", "$0.50-1.00", "$1.00-5.00", "$5.00+"];
|
|
258
259
|
for (var b = 0; b < bucketOrder.length; b++) {
|
|
259
260
|
costBuckets.set(bucketOrder[b], 0);
|
|
260
261
|
}
|
|
@@ -335,7 +336,9 @@ function aggregate(sessions: SessionData[], period: AnalyticsPeriod): AnalyticsP
|
|
|
335
336
|
});
|
|
336
337
|
|
|
337
338
|
var bucket = getCostBucket(sess.cost);
|
|
338
|
-
|
|
339
|
+
if (bucket) {
|
|
340
|
+
costBuckets.set(bucket, (costBuckets.get(bucket) || 0) + 1);
|
|
341
|
+
}
|
|
339
342
|
}
|
|
340
343
|
|
|
341
344
|
var totalTokensAll = totalInput + totalOutput + totalCacheRead + totalCacheCreation;
|
|
@@ -388,7 +391,8 @@ function aggregate(sessions: SessionData[], period: AnalyticsPeriod): AnalyticsP
|
|
|
388
391
|
}
|
|
389
392
|
|
|
390
393
|
var sessionBubbles: AnalyticsPayload["sessionBubbles"] = [];
|
|
391
|
-
var
|
|
394
|
+
var nonZeroCost = filtered.filter(function (s) { return s.cost > 0; });
|
|
395
|
+
var sorted = nonZeroCost.slice().sort(function (a, b) {
|
|
392
396
|
return (b.endTime || b.startTime) - (a.endTime || a.startTime);
|
|
393
397
|
});
|
|
394
398
|
var bubbleCap = Math.min(sorted.length, 200);
|
|
@@ -572,7 +576,7 @@ function aggregate(sessions: SessionData[], period: AnalyticsPeriod): AnalyticsP
|
|
|
572
576
|
|
|
573
577
|
var sessionTimeline: AnalyticsPayload["sessionTimeline"] = [];
|
|
574
578
|
var tlSorted = filtered
|
|
575
|
-
.filter(function (s) { return s.startTime > 0 && s.endTime > 0; })
|
|
579
|
+
.filter(function (s) { return s.startTime > 0 && s.endTime > 0 && s.cost > 0; })
|
|
576
580
|
.sort(function (a, b) { return b.startTime - a.startTime; });
|
|
577
581
|
var tlCap = Math.min(tlSorted.length, 50);
|
|
578
582
|
for (var tli = 0; tli < tlCap; tli++) {
|