@cryptiklemur/lattice 1.8.0 → 1.10.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.
@@ -1,5 +1,6 @@
1
1
  import { Component } from "react";
2
2
  import { useAnalytics } from "../../hooks/useAnalytics";
3
+ import { useMesh } from "../../hooks/useMesh";
3
4
 
4
5
  class ChartErrorBoundary extends Component<{ children: React.ReactNode; name: string }, { error: Error | null }> {
5
6
  constructor(props: { children: React.ReactNode; name: string }) {
@@ -41,9 +42,12 @@ import { ToolSunburst } from "./charts/ToolSunburst";
41
42
  import { PermissionBreakdown } from "./charts/PermissionBreakdown";
42
43
  import { ProjectRadar } from "./charts/ProjectRadar";
43
44
  import { SessionComplexityList } from "./charts/SessionComplexityList";
45
+ import { NodeFleetOverview } from "./charts/NodeFleetOverview";
44
46
 
45
47
  export function AnalyticsView() {
46
48
  var analytics = useAnalytics();
49
+ var mesh = useMesh();
50
+ var nodes = mesh.nodes;
47
51
 
48
52
  return (
49
53
  <div className="flex flex-col h-full overflow-hidden bg-base-100 bg-lattice-grid">
@@ -173,6 +177,12 @@ export function AnalyticsView() {
173
177
  <SessionComplexityList data={analytics.data.sessionComplexity} />
174
178
  </ChartErrorBoundary>
175
179
  </ChartCard>
180
+
181
+ <ChartCard title="Node Fleet">
182
+ <ChartErrorBoundary name="Fleet">
183
+ <NodeFleetOverview nodes={nodes} />
184
+ </ChartErrorBoundary>
185
+ </ChartCard>
176
186
  </div>
177
187
  )}
178
188
 
@@ -1,3 +1,5 @@
1
+ import { useState, useEffect, useRef } from "react";
2
+ import { Maximize2, Minimize2 } from "lucide-react";
1
3
  import type { ReactNode } from "react";
2
4
 
3
5
  interface ChartCardProps {
@@ -7,16 +9,148 @@ interface ChartCardProps {
7
9
  action?: ReactNode;
8
10
  }
9
11
 
10
- export function ChartCard({ title, children, className, action }: ChartCardProps) {
11
- return (
12
- <div className={["rounded-xl border border-base-content/8 bg-base-300/50 p-4", className].filter(Boolean).join(" ")}>
12
+ export function ChartCard(props: ChartCardProps) {
13
+ var [isFullscreen, setIsFullscreen] = useState(false);
14
+ var cardRef = useRef<HTMLDivElement>(null);
15
+ var [originRect, setOriginRect] = useState<DOMRect | null>(null);
16
+ var [animating, setAnimating] = useState(false);
17
+
18
+ function openFullscreen() {
19
+ if (cardRef.current) {
20
+ setOriginRect(cardRef.current.getBoundingClientRect());
21
+ }
22
+ setAnimating(true);
23
+ setIsFullscreen(true);
24
+ requestAnimationFrame(function () {
25
+ requestAnimationFrame(function () {
26
+ setAnimating(false);
27
+ });
28
+ });
29
+ }
30
+
31
+ function closeFullscreen() {
32
+ setAnimating(true);
33
+ setTimeout(function () {
34
+ setIsFullscreen(false);
35
+ setAnimating(false);
36
+ setOriginRect(null);
37
+ }, 250);
38
+ }
39
+
40
+ useEffect(function () {
41
+ if (!isFullscreen) return;
42
+ function handleKeyDown(e: KeyboardEvent) {
43
+ if (e.key === "Escape") closeFullscreen();
44
+ }
45
+ document.addEventListener("keydown", handleKeyDown);
46
+ return function () { document.removeEventListener("keydown", handleKeyDown); };
47
+ }, [isFullscreen]);
48
+
49
+ useEffect(function () {
50
+ if (isFullscreen) {
51
+ document.body.style.overflow = "hidden";
52
+ } else {
53
+ document.body.style.overflow = "";
54
+ }
55
+ return function () { document.body.style.overflow = ""; };
56
+ }, [isFullscreen]);
57
+
58
+ var cardContent = (
59
+ <>
13
60
  <div className="flex items-center justify-between mb-4">
14
61
  <span className="text-[10px] font-mono font-bold uppercase tracking-widest text-base-content/35">
15
- {title}
62
+ {props.title}
16
63
  </span>
17
- {action && <div>{action}</div>}
64
+ <div className="flex items-center gap-2">
65
+ {props.action && <div>{props.action}</div>}
66
+ <button
67
+ onClick={function () {
68
+ if (isFullscreen) {
69
+ closeFullscreen();
70
+ } else {
71
+ openFullscreen();
72
+ }
73
+ }}
74
+ className="text-base-content/20 hover:text-base-content/50 transition-colors cursor-pointer p-0.5 rounded hover:bg-base-content/5"
75
+ aria-label={isFullscreen ? "Exit fullscreen" : "Fullscreen"}
76
+ title={isFullscreen ? "Exit fullscreen (Esc)" : "Fullscreen"}
77
+ >
78
+ {isFullscreen ? <Minimize2 size={12} /> : <Maximize2 size={12} />}
79
+ </button>
80
+ </div>
18
81
  </div>
19
- {children}
82
+ {props.children}
83
+ </>
84
+ );
85
+
86
+ if (isFullscreen) {
87
+ var overlayStyle: React.CSSProperties = {
88
+ transition: "opacity 250ms cubic-bezier(0.4, 0, 0.2, 1)",
89
+ opacity: animating ? 0 : 1,
90
+ };
91
+
92
+ var modalStyle: React.CSSProperties = {
93
+ transition: "all 250ms cubic-bezier(0.4, 0, 0.2, 1)",
94
+ };
95
+
96
+ if (animating && originRect) {
97
+ modalStyle.position = "fixed";
98
+ modalStyle.top = originRect.top + "px";
99
+ modalStyle.left = originRect.left + "px";
100
+ modalStyle.width = originRect.width + "px";
101
+ modalStyle.height = originRect.height + "px";
102
+ modalStyle.opacity = 0;
103
+ }
104
+
105
+ return (
106
+ <>
107
+ <div
108
+ ref={cardRef}
109
+ className={"rounded-xl border border-base-content/8 bg-base-300/50 p-4 invisible " + (props.className || "")}
110
+ >
111
+ {cardContent}
112
+ </div>
113
+
114
+ <div
115
+ className="fixed inset-0 z-[9998] bg-black/60 backdrop-blur-sm"
116
+ style={overlayStyle}
117
+ onClick={closeFullscreen}
118
+ />
119
+ <div
120
+ className="fixed inset-4 sm:inset-8 z-[9999] rounded-2xl border border-base-content/10 bg-base-200 shadow-2xl overflow-hidden flex flex-col"
121
+ style={animating ? { opacity: 0, transform: "scale(0.95)", transition: "all 250ms cubic-bezier(0.4, 0, 0.2, 1)" } : { opacity: 1, transform: "scale(1)", transition: "all 250ms cubic-bezier(0.4, 0, 0.2, 1)" }}
122
+ >
123
+ <div className="flex items-center justify-between px-6 py-4 border-b border-base-content/8 flex-shrink-0">
124
+ <span className="text-[12px] font-mono font-bold uppercase tracking-widest text-base-content/50">
125
+ {props.title}
126
+ </span>
127
+ <div className="flex items-center gap-3">
128
+ {props.action && <div>{props.action}</div>}
129
+ <button
130
+ onClick={closeFullscreen}
131
+ className="text-base-content/30 hover:text-base-content/60 transition-colors cursor-pointer p-1 rounded-lg hover:bg-base-content/5"
132
+ aria-label="Exit fullscreen"
133
+ >
134
+ <Minimize2 size={16} />
135
+ </button>
136
+ </div>
137
+ </div>
138
+ <div className="flex-1 p-6 overflow-auto flex items-center justify-center">
139
+ <div className="w-full h-full">
140
+ {props.children}
141
+ </div>
142
+ </div>
143
+ </div>
144
+ </>
145
+ );
146
+ }
147
+
148
+ return (
149
+ <div
150
+ ref={cardRef}
151
+ className={"rounded-xl border border-base-content/8 bg-base-300/50 p-4 " + (props.className || "")}
152
+ >
153
+ {cardContent}
20
154
  </div>
21
155
  );
22
156
  }
@@ -0,0 +1,88 @@
1
+ import { Server, Wifi, WifiOff } from "lucide-react";
2
+ import type { NodeInfo } from "@lattice/shared";
3
+
4
+ interface NodeFleetOverviewProps {
5
+ nodes: NodeInfo[];
6
+ }
7
+
8
+ export function NodeFleetOverview(props: NodeFleetOverviewProps) {
9
+ var nodes = props.nodes;
10
+
11
+ if (nodes.length === 0) {
12
+ return (
13
+ <div className="flex items-center justify-center h-[200px] text-base-content/30 font-mono text-[12px]">
14
+ No nodes connected
15
+ </div>
16
+ );
17
+ }
18
+
19
+ return (
20
+ <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
21
+ {nodes.map(function (node) {
22
+ return (
23
+ <div
24
+ key={node.id}
25
+ className={
26
+ "rounded-lg border p-3.5 transition-colors " +
27
+ (node.online
28
+ ? "border-success/20 bg-success/[0.03]"
29
+ : "border-base-content/8 bg-base-content/[0.02] opacity-60")
30
+ }
31
+ >
32
+ <div className="flex items-center gap-2.5 mb-2.5">
33
+ <Server size={14} className={node.online ? "text-success/60" : "text-base-content/25"} />
34
+ <span className="text-[13px] font-mono font-medium text-base-content/80 truncate">{node.name}</span>
35
+ <span className="ml-auto flex-shrink-0">
36
+ {node.online
37
+ ? <Wifi size={12} className="text-success/50" />
38
+ : <WifiOff size={12} className="text-base-content/20" />
39
+ }
40
+ </span>
41
+ </div>
42
+
43
+ <div className="flex flex-col gap-1">
44
+ <div className="flex items-center justify-between text-[10px] font-mono">
45
+ <span className="text-base-content/35 uppercase tracking-wider">Address</span>
46
+ <span className="text-base-content/50">{node.address}:{node.port}</span>
47
+ </div>
48
+ <div className="flex items-center justify-between text-[10px] font-mono">
49
+ <span className="text-base-content/35 uppercase tracking-wider">Projects</span>
50
+ <span className="text-base-content/50">{node.projects.length}</span>
51
+ </div>
52
+ <div className="flex items-center justify-between text-[10px] font-mono">
53
+ <span className="text-base-content/35 uppercase tracking-wider">Status</span>
54
+ <span className={node.online ? "text-success/70" : "text-base-content/30"}>
55
+ {node.online ? "Online" : "Offline"}
56
+ </span>
57
+ </div>
58
+ {node.isLocal && (
59
+ <div className="flex items-center justify-between text-[10px] font-mono">
60
+ <span className="text-base-content/35 uppercase tracking-wider">Type</span>
61
+ <span className="text-primary/50">Local</span>
62
+ </div>
63
+ )}
64
+ </div>
65
+
66
+ {node.projects.length > 0 && (
67
+ <div className="mt-2.5 pt-2 border-t border-base-content/5">
68
+ <div className="text-[9px] font-mono text-base-content/25 uppercase tracking-widest mb-1.5">Projects</div>
69
+ <div className="flex flex-wrap gap-1">
70
+ {node.projects.map(function (p) {
71
+ return (
72
+ <span
73
+ key={p.slug}
74
+ className="px-1.5 py-0.5 rounded text-[9px] font-mono bg-base-content/5 text-base-content/40"
75
+ >
76
+ {p.slug}
77
+ </span>
78
+ );
79
+ })}
80
+ </div>
81
+ </div>
82
+ )}
83
+ </div>
84
+ );
85
+ })}
86
+ </div>
87
+ );
88
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cryptiklemur/lattice",
3
- "version": "1.8.0",
3
+ "version": "1.10.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>",