@agenticmail/enterprise 0.5.214 → 0.5.216

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.
@@ -2,336 +2,245 @@ import { h, useState, useEffect, useCallback, useRef, Fragment, useApp, engineCa
2
2
  import { I } from '../components/icons.js';
3
3
  import { HelpButton } from '../components/help-button.js';
4
4
 
5
+ // ─── Inject theme CSS once ───────────────────────────────
6
+ var _injected = false;
7
+ function injectCSS() {
8
+ if (_injected) return; _injected = true;
9
+ var s = document.createElement('style');
10
+ s.textContent = `
11
+ :root { --oc-bg: var(--bg-primary, #0a0c14); --oc-text: var(--text-primary, #fff); --oc-dim: var(--text-muted, rgba(255,255,255,0.45)); --oc-faint: rgba(255,255,255,0.12); --oc-card: rgba(255,255,255,0.02); --oc-card-h: rgba(255,255,255,0.06); --oc-toolbar: rgba(0,0,0,0.3); --oc-border: rgba(255,255,255,0.08); --oc-edge: rgba(255,255,255,0.25); --oc-edge-dim: rgba(255,255,255,0.06); --oc-tip-bg: rgba(15,17,23,0.95); --oc-btn-bg: rgba(255,255,255,0.08); --oc-btn-border: rgba(255,255,255,0.12); --oc-metrics: rgba(0,0,0,0.12); }
12
+ [data-theme="light"], :root:not(.dark) { --oc-bg: var(--bg-primary, #f8fafc); --oc-text: var(--text-primary, #1e293b); --oc-dim: var(--text-muted, #64748b); --oc-faint: var(--border, rgba(0,0,0,0.08)); --oc-card: rgba(0,0,0,0.02); --oc-card-h: rgba(0,0,0,0.05); --oc-toolbar: rgba(0,0,0,0.03); --oc-border: var(--border, rgba(0,0,0,0.08)); --oc-edge: rgba(0,0,0,0.2); --oc-edge-dim: rgba(0,0,0,0.05); --oc-tip-bg: var(--bg-primary, #fff); --oc-btn-bg: var(--bg-secondary, rgba(0,0,0,0.04)); --oc-btn-border: var(--border, rgba(0,0,0,0.1)); --oc-metrics: rgba(0,0,0,0.02); }
13
+ @media (prefers-color-scheme: light) { :root:not(.dark) { --oc-bg: var(--bg-primary, #f8fafc); --oc-text: var(--text-primary, #1e293b); --oc-dim: var(--text-muted, #64748b); --oc-faint: var(--border, rgba(0,0,0,0.08)); --oc-card: rgba(0,0,0,0.02); --oc-card-h: rgba(0,0,0,0.05); --oc-toolbar: rgba(0,0,0,0.03); --oc-border: var(--border, rgba(0,0,0,0.08)); --oc-edge: rgba(0,0,0,0.2); --oc-edge-dim: rgba(0,0,0,0.05); --oc-tip-bg: var(--bg-primary, #fff); --oc-btn-bg: var(--bg-secondary, rgba(0,0,0,0.04)); --oc-btn-border: var(--border, rgba(0,0,0,0.1)); --oc-metrics: rgba(0,0,0,0.02); } }
14
+ `;
15
+ document.head.appendChild(s);
16
+ }
17
+
5
18
  // ─── Layout Constants ────────────────────────────────────
6
- const NODE_W = 220;
7
- const NODE_H = 72;
8
- const H_GAP = 40; // horizontal gap between siblings
9
- const V_GAP = 80; // vertical gap between levels
10
- const PAD = 60; // canvas padding
19
+ var NODE_W = 220;
20
+ var NODE_H = 72;
21
+ var H_GAP = 40;
22
+ var V_GAP = 80;
23
+ var PAD = 60;
11
24
 
12
- // ─── Colors ──────────────────────────────────────────────
13
- const STATE_COLORS = {
14
- running: '#22c55e',
15
- stopped: '#6b7394',
16
- error: '#ef4444',
17
- paused: '#f59e0b',
18
- deploying: '#06b6d4',
19
- };
20
- const ACCENT = '#6366f1';
21
- const EDGE_COLOR = 'rgba(255,255,255,0.25)';
22
- const EDGE_HIGHLIGHT = 'rgba(99,102,241,0.7)';
23
- const BG = '#0a0c14';
25
+ var STATE_COLORS = { running: '#22c55e', stopped: '#6b7394', error: '#ef4444', paused: '#f59e0b', deploying: '#06b6d4' };
26
+ var ACCENT = '#6366f1';
24
27
 
25
- // ─── Tree Layout (Reingold-Tilford inspired, simplified) ─
28
+ // ─── Tree Layout ─────────────────────────────────────────
26
29
  function layoutTree(nodes) {
27
30
  if (!nodes || !nodes.length) return { positioned: [], width: 0, height: 0 };
28
-
29
- const byId = new Map();
30
- nodes.forEach(n => byId.set(n.agentId, { ...n, children: [], x: 0, y: 0, subtreeW: 0 }));
31
-
32
- // Build parent→children, detect roots
33
- const roots = [];
34
- for (const n of byId.values()) {
35
- if (n.managerId && byId.has(n.managerId)) {
36
- byId.get(n.managerId).children.push(n);
37
- } else {
38
- roots.push(n);
39
- }
31
+ var byId = new Map();
32
+ nodes.forEach(function(n) { byId.set(n.agentId, Object.assign({}, n, { children: [], x: 0, y: 0, subtreeW: 0 })); });
33
+ var roots = [];
34
+ for (var n of byId.values()) {
35
+ if (n.managerId && byId.has(n.managerId)) byId.get(n.managerId).children.push(n);
36
+ else roots.push(n);
40
37
  }
41
-
42
- // Also add external-manager virtual nodes
43
- const externalManagers = new Map();
44
- for (const n of byId.values()) {
45
- if (n.managerType === 'external' && n.managerName) {
46
- const key = 'ext-' + (n.managerEmail || n.managerName);
47
- if (!externalManagers.has(key)) {
48
- externalManagers.set(key, {
49
- agentId: key,
50
- name: n.managerName,
51
- role: 'External Manager',
52
- state: 'external',
53
- managerType: 'none',
54
- managerId: null,
55
- subordinateIds: [],
56
- subordinateCount: 0,
57
- isManager: true,
58
- level: -1,
59
- clockedIn: true,
60
- activeTasks: 0,
61
- errorsToday: 0,
62
- isExternal: true,
63
- children: [],
64
- x: 0, y: 0, subtreeW: 0,
65
- });
66
- }
67
- const extNode = externalManagers.get(key);
68
- // Remove from roots, add as child of external
69
- const idx = roots.indexOf(n);
38
+ var externalManagers = new Map();
39
+ for (var n2 of byId.values()) {
40
+ if (n2.managerType === 'external' && n2.managerName) {
41
+ var key = 'ext-' + (n2.managerEmail || n2.managerName);
42
+ if (!externalManagers.has(key)) externalManagers.set(key, { agentId: key, name: n2.managerName, role: 'External Manager', state: 'external', managerType: 'none', managerId: null, subordinateIds: [], subordinateCount: 0, isManager: true, level: -1, clockedIn: true, activeTasks: 0, errorsToday: 0, isExternal: true, children: [], x: 0, y: 0, subtreeW: 0 });
43
+ var ext = externalManagers.get(key);
44
+ var idx = roots.indexOf(n2);
70
45
  if (idx >= 0) roots.splice(idx, 1);
71
- extNode.children.push(n);
46
+ ext.children.push(n2);
72
47
  }
73
48
  }
74
- // Add external managers as roots
75
- for (const ext of externalManagers.values()) {
76
- if (ext.children.length > 0) roots.push(ext);
77
- }
49
+ for (var e of externalManagers.values()) { if (e.children.length > 0) roots.push(e); }
50
+ if (roots.length === 0 && byId.size > 0) roots.push.apply(roots, Array.from(byId.values()));
78
51
 
79
- if (roots.length === 0 && byId.size > 0) {
80
- // No clear root just pick all as roots
81
- roots.push(...byId.values());
82
- }
83
-
84
- // Pass 1: compute subtree widths bottom-up
85
- function computeWidth(node) {
86
- if (node.children.length === 0) {
87
- node.subtreeW = NODE_W;
88
- return NODE_W;
89
- }
90
- let total = 0;
91
- node.children.forEach(c => { total += computeWidth(c); });
92
- total += (node.children.length - 1) * H_GAP;
93
- node.subtreeW = Math.max(NODE_W, total);
94
- return node.subtreeW;
52
+ function computeW(node) {
53
+ if (node.children.length === 0) { node.subtreeW = NODE_W; return NODE_W; }
54
+ var t = 0; node.children.forEach(function(c) { t += computeW(c); }); t += (node.children.length - 1) * H_GAP;
55
+ node.subtreeW = Math.max(NODE_W, t); return node.subtreeW;
95
56
  }
96
-
97
- // Pass 2: assign positions top-down
98
- function assignPositions(node, x, y) {
99
- node.x = x + node.subtreeW / 2 - NODE_W / 2;
100
- node.y = y;
57
+ function assignPos(node, x, y) {
58
+ node.x = x + node.subtreeW / 2 - NODE_W / 2; node.y = y;
101
59
  if (node.children.length === 0) return;
102
- let childX = x;
103
- const childrenTotalW = node.children.reduce((s, c) => s + c.subtreeW, 0) + (node.children.length - 1) * H_GAP;
104
- // Center children under parent
105
- childX = node.x + NODE_W / 2 - childrenTotalW / 2;
106
- node.children.forEach(c => {
107
- assignPositions(c, childX, y + NODE_H + V_GAP);
108
- childX += c.subtreeW + H_GAP;
109
- });
110
- }
111
-
112
- // Layout all root trees side by side
113
- let totalW = 0;
114
- roots.forEach(r => { totalW += computeWidth(r); });
115
- totalW += (roots.length - 1) * H_GAP * 2;
116
-
117
- let cx = PAD;
118
- roots.forEach(r => {
119
- assignPositions(r, cx, PAD);
120
- cx += r.subtreeW + H_GAP * 2;
121
- });
122
-
123
- // Flatten
124
- const positioned = [];
125
- let maxX = 0, maxY = 0;
126
- function flatten(node) {
127
- positioned.push(node);
128
- maxX = Math.max(maxX, node.x + NODE_W);
129
- maxY = Math.max(maxY, node.y + NODE_H);
130
- node.children.forEach(flatten);
60
+ var cw = node.children.reduce(function(s, c) { return s + c.subtreeW; }, 0) + (node.children.length - 1) * H_GAP;
61
+ var cx = node.x + NODE_W / 2 - cw / 2;
62
+ node.children.forEach(function(c) { assignPos(c, cx, y + NODE_H + V_GAP); cx += c.subtreeW + H_GAP; });
131
63
  }
64
+ roots.forEach(function(r) { computeW(r); });
65
+ var cx = PAD; roots.forEach(function(r) { assignPos(r, cx, PAD); cx += r.subtreeW + H_GAP * 2; });
66
+ var positioned = [], maxX = 0, maxY = 0;
67
+ function flatten(node) { positioned.push(node); maxX = Math.max(maxX, node.x + NODE_W); maxY = Math.max(maxY, node.y + NODE_H); node.children.forEach(flatten); }
132
68
  roots.forEach(flatten);
133
-
134
- return { positioned, width: maxX + PAD, height: maxY + PAD + 40 };
69
+ return { positioned: positioned, width: maxX + PAD, height: maxY + PAD + 40 };
135
70
  }
136
71
 
137
- // ─── SVG Edge Path (curved, child→parent = reports to) ──
138
72
  function edgePath(parent, child) {
139
- const x1 = child.x + NODE_W / 2;
140
- const y1 = child.y;
141
- const x2 = parent.x + NODE_W / 2;
142
- const y2 = parent.y + NODE_H;
143
- const midY = y1 + (y2 - y1) * 0.5;
144
- return `M ${x1} ${y1} C ${x1} ${midY}, ${x2} ${midY}, ${x2} ${y2}`;
73
+ var x1 = child.x + NODE_W / 2, y1 = child.y, x2 = parent.x + NODE_W / 2, y2 = parent.y + NODE_H;
74
+ var midY = y1 + (y2 - y1) * 0.5;
75
+ return 'M ' + x1 + ' ' + y1 + ' C ' + x1 + ' ' + midY + ', ' + x2 + ' ' + midY + ', ' + x2 + ' ' + y2;
76
+ }
77
+
78
+ // ─── Helpers ─────────────────────────────────────────────
79
+ var toolbarBtnStyle = { background: 'var(--oc-btn-bg)', border: '1px solid var(--oc-btn-border)', borderRadius: 6, color: 'var(--oc-text)', fontSize: 14, fontWeight: 600, padding: '4px 8px', cursor: 'pointer', lineHeight: '1.2' };
80
+ function tagStyle(color) { return { fontSize: 9, fontWeight: 600, padding: '1px 5px', borderRadius: 4, background: color + '22', color: color, letterSpacing: '0.02em' }; }
81
+ function tooltipRow(label, value, color) {
82
+ return h('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', fontSize: 11 } },
83
+ h('span', { style: { color: 'var(--oc-dim)' } }, label),
84
+ h('span', { style: { fontWeight: 600, color: color || 'var(--oc-text)' } }, value));
85
+ }
86
+ function legendDot(color, label) {
87
+ return h('div', { style: { display: 'flex', alignItems: 'center', gap: 4 } },
88
+ h('div', { style: { width: 8, height: 8, borderRadius: '50%', background: color } }),
89
+ h('span', { style: { color: 'var(--oc-dim)', fontSize: 12 } }, label));
90
+ }
91
+ function timeAgo(iso) {
92
+ var diff = Date.now() - new Date(iso).getTime();
93
+ var mins = Math.floor(diff / 60000);
94
+ if (mins < 1) return 'Just now'; if (mins < 60) return mins + 'm ago';
95
+ var hrs = Math.floor(mins / 60); if (hrs < 24) return hrs + 'h ago';
96
+ return Math.floor(hrs / 24) + 'd ago';
97
+ }
98
+
99
+ // ─── Summary Metrics ─────────────────────────────────────
100
+ function OrgSummary(props) {
101
+ var nodes = props.nodes;
102
+ var running = 0, stopped = 0, errored = 0, paused = 0, external = 0, managers = 0, totalTasks = 0, totalErrors = 0;
103
+ nodes.forEach(function(n) {
104
+ if (n.isExternal) { external++; return; }
105
+ if (n.state === 'running') running++;
106
+ else if (n.state === 'error') errored++;
107
+ else if (n.state === 'paused') paused++;
108
+ else stopped++;
109
+ if (n.isManager || (n.children && n.children.length > 0)) managers++;
110
+ totalTasks += n.activeTasks || 0;
111
+ totalErrors += n.errorsToday || 0;
112
+ });
113
+ function chip(label, value, color) {
114
+ return h('div', { style: { display: 'flex', alignItems: 'center', gap: 4, padding: '3px 8px', background: 'var(--oc-card)', borderRadius: 6 } },
115
+ h('span', { style: { fontSize: 10, color: 'var(--oc-dim)' } }, label),
116
+ h('span', { style: { fontSize: 11, fontWeight: 700, color: color } }, value));
117
+ }
118
+ return h('div', { style: { display: 'flex', alignItems: 'center', gap: 6, padding: '6px 16px', borderBottom: '1px solid var(--oc-border)', background: 'var(--oc-metrics)', flexShrink: 0, overflowX: 'auto', fontSize: 11 } },
119
+ chip('Agents', nodes.length - external, 'var(--oc-text)'),
120
+ chip('Running', running, '#22c55e'),
121
+ stopped > 0 && chip('Stopped', stopped, '#6b7394'),
122
+ errored > 0 && chip('Error', errored, '#ef4444'),
123
+ paused > 0 && chip('Paused', paused, '#f59e0b'),
124
+ external > 0 && chip('Human', external, '#8b5cf6'),
125
+ managers > 0 && chip('Managers', managers, ACCENT),
126
+ totalTasks > 0 && h(Fragment, null,
127
+ h('div', { style: { width: 1, height: 14, background: 'var(--oc-faint)' } }),
128
+ chip('Active Tasks', totalTasks, '#f59e0b')
129
+ ),
130
+ totalErrors > 0 && chip('Errors Today', totalErrors, '#ef4444')
131
+ );
145
132
  }
146
133
 
147
134
  // ─── Main Component ─────────────────────────────────────
148
135
  export function OrgChartPage() {
149
- const { toast } = useApp();
150
- const [nodes, setNodes] = useState([]);
151
- const [loading, setLoading] = useState(true);
152
- const [error, setError] = useState(null);
153
- const [hoveredId, setHoveredId] = useState(null);
154
- const [zoom, setZoom] = useState(1);
155
- const [pan, setPan] = useState({ x: 0, y: 0 });
156
- const [dragging, setDragging] = useState(false);
157
- const [dragStart, setDragStart] = useState({ x: 0, y: 0 });
158
- const [mousePos, setMousePos] = useState({ x: 0, y: 0 });
159
- const containerRef = useRef(null);
160
-
161
- const load = useCallback(async () => {
162
- setLoading(true);
163
- setError(null);
164
- try {
165
- // Fetch agents (for avatars) alongside hierarchy
166
- const [hierRes, agentRes] = await Promise.all([
167
- engineCall('/hierarchy/org-chart').catch(() => null),
168
- engineCall('/agents?orgId=' + getOrgId()).catch(() => ({ agents: [] })),
169
- ]);
170
- const avatarMap = {};
171
- (agentRes.agents || []).forEach(a => {
172
- avatarMap[a.id] = a.config?.identity?.avatar || a.config?.avatar || a.config?.persona?.avatar || null;
173
- });
136
+ injectCSS();
137
+ var app = useApp();
138
+ var toast = app.toast;
139
+ var _nodes = useState([]); var nodes = _nodes[0]; var setNodes = _nodes[1];
140
+ var _loading = useState(true); var loading = _loading[0]; var setLoading = _loading[1];
141
+ var _error = useState(null); var error = _error[0]; var setError = _error[1];
142
+ var _hoveredId = useState(null); var hoveredId = _hoveredId[0]; var setHoveredId = _hoveredId[1];
143
+ var _zoom = useState(1); var zoom = _zoom[0]; var setZoom = _zoom[1];
144
+ var _pan = useState({ x: 0, y: 0 }); var pan = _pan[0]; var setPan = _pan[1];
145
+ var _dragging = useState(false); var dragging = _dragging[0]; var setDragging = _dragging[1];
146
+ var _dragStart = useState({ x: 0, y: 0 }); var dragStart = _dragStart[0]; var setDragStart = _dragStart[1];
147
+ var _mousePos = useState({ x: 0, y: 0 }); var mousePos = _mousePos[0]; var setMousePos = _mousePos[1];
148
+ var containerRef = useRef(null);
149
+
150
+ var load = useCallback(function() {
151
+ setLoading(true); setError(null);
152
+ Promise.all([
153
+ engineCall('/hierarchy/org-chart').catch(function() { return null; }),
154
+ engineCall('/agents?orgId=' + getOrgId()).catch(function() { return { agents: [] }; }),
155
+ ]).then(function(res) {
156
+ var hierRes = res[0]; var agentRes = res[1];
157
+ var avatarMap = {};
158
+ (agentRes.agents || []).forEach(function(a) { avatarMap[a.id] = a.config?.identity?.avatar || a.config?.avatar || a.config?.persona?.avatar || null; });
174
159
  if (hierRes && hierRes.nodes) {
175
- setNodes(hierRes.nodes.map(n => ({ ...n, avatar: avatarMap[n.agentId] || null })));
160
+ setNodes(hierRes.nodes.map(function(n) { return Object.assign({}, n, { avatar: avatarMap[n.agentId] || null }); }));
176
161
  } else {
177
- // Fallback: build from agents list
178
- const agents = agentRes.agents || [];
179
- setNodes(agents.map(a => ({
180
- agentId: a.id,
181
- name: a.config?.name || a.id,
182
- role: a.config?.role || 'Agent',
183
- state: a.state || 'stopped',
184
- managerId: a.config?.managerId || null,
185
- managerType: a.config?.externalManagerEmail ? 'external' : a.config?.managerId ? 'internal' : 'none',
186
- managerName: a.config?.externalManagerName || null,
187
- managerEmail: a.config?.externalManagerEmail || null,
188
- subordinateIds: [],
189
- subordinateCount: 0,
190
- isManager: false,
191
- level: 0,
192
- clockedIn: a.state === 'running',
193
- activeTasks: 0,
194
- errorsToday: 0,
195
- avatar: a.config?.identity?.avatar || a.config?.avatar || a.config?.persona?.avatar || null,
196
- comm: a.config?.comm || {},
197
- })));
162
+ var agents = agentRes.agents || [];
163
+ setNodes(agents.map(function(a) { return { agentId: a.id, name: a.config?.name || a.id, role: a.config?.role || 'Agent', state: a.state || 'stopped', managerId: a.config?.managerId || null, managerType: a.config?.externalManagerEmail ? 'external' : a.config?.managerId ? 'internal' : 'none', managerName: a.config?.externalManagerName || null, managerEmail: a.config?.externalManagerEmail || null, subordinateIds: [], subordinateCount: 0, isManager: false, level: 0, clockedIn: a.state === 'running', activeTasks: 0, errorsToday: 0, avatar: avatarMap[a.id] || null, comm: a.config?.comm || {} }; }));
198
164
  }
199
- } catch (e) {
200
- setError(e.message || 'Failed to load hierarchy');
201
- }
165
+ }).catch(function(e) { setError(e.message || 'Failed to load'); });
202
166
  setLoading(false);
203
167
  }, []);
204
168
 
205
- useEffect(() => { load(); }, [load]);
206
-
207
- // Layout
208
- const { positioned, width: treeW, height: treeH } = layoutTree(nodes);
209
-
210
- // Zoom
211
- const handleWheel = useCallback((e) => {
212
- e.preventDefault();
213
- const delta = e.deltaY > 0 ? -0.08 : 0.08;
214
- setZoom(z => Math.min(3, Math.max(0.15, z + delta)));
215
- }, []);
216
-
217
- // Pan
218
- const handleMouseDown = useCallback((e) => {
219
- if (e.button !== 0) return;
220
- // Only start drag on background (not on nodes)
221
- if (e.target.closest('.org-node')) return;
222
- setDragging(true);
223
- setDragStart({ x: e.clientX - pan.x, y: e.clientY - pan.y });
224
- }, [pan]);
225
-
226
- const handleMouseMove = useCallback((e) => {
227
- if (!dragging) return;
228
- setPan({ x: e.clientX - dragStart.x, y: e.clientY - dragStart.y });
229
- }, [dragging, dragStart]);
169
+ useEffect(function() { load(); }, []);
230
170
 
231
- const handleMouseUp = useCallback(() => { setDragging(false); }, []);
171
+ var layout = layoutTree(nodes);
172
+ var positioned = layout.positioned; var treeW = layout.width; var treeH = layout.height;
232
173
 
233
- useEffect(() => {
234
- if (dragging) {
235
- window.addEventListener('mousemove', handleMouseMove);
236
- window.addEventListener('mouseup', handleMouseUp);
237
- return () => { window.removeEventListener('mousemove', handleMouseMove); window.removeEventListener('mouseup', handleMouseUp); };
238
- }
174
+ var handleWheel = useCallback(function(e) { e.preventDefault(); setZoom(function(z) { return Math.min(3, Math.max(0.15, z + (e.deltaY > 0 ? -0.08 : 0.08))); }); }, []);
175
+ var handleMouseDown = useCallback(function(e) { if (e.button !== 0 || e.target.closest('.org-node')) return; setDragging(true); setDragStart({ x: e.clientX - pan.x, y: e.clientY - pan.y }); }, [pan]);
176
+ var handleMouseMove = useCallback(function(e) { if (!dragging) return; setPan({ x: e.clientX - dragStart.x, y: e.clientY - dragStart.y }); }, [dragging, dragStart]);
177
+ var handleMouseUp = useCallback(function() { setDragging(false); }, []);
178
+ useEffect(function() {
179
+ if (dragging) { window.addEventListener('mousemove', handleMouseMove); window.addEventListener('mouseup', handleMouseUp); return function() { window.removeEventListener('mousemove', handleMouseMove); window.removeEventListener('mouseup', handleMouseUp); }; }
239
180
  }, [dragging, handleMouseMove, handleMouseUp]);
240
181
 
241
- // Fit to view
242
- const fitToView = useCallback(() => {
182
+ var fitToView = useCallback(function() {
243
183
  if (!containerRef.current || !treeW || !treeH) return;
244
- const rect = containerRef.current.getBoundingClientRect();
245
- const scaleX = (rect.width - 40) / treeW;
246
- const scaleY = (rect.height - 40) / treeH;
247
- const scale = Math.min(scaleX, scaleY, 1.5);
248
- setZoom(scale);
249
- setPan({
250
- x: (rect.width - treeW * scale) / 2,
251
- y: (rect.height - treeH * scale) / 2,
252
- });
184
+ var rect = containerRef.current.getBoundingClientRect();
185
+ var scale = Math.min((rect.width - 40) / treeW, (rect.height - 40) / treeH, 1.5);
186
+ setZoom(scale); setPan({ x: (rect.width - treeW * scale) / 2, y: (rect.height - treeH * scale) / 2 });
253
187
  }, [treeW, treeH]);
254
-
255
- useEffect(() => { if (positioned.length > 0) fitToView(); }, [positioned.length]);
256
-
257
- // Get ancestors + descendants for highlighting
258
- const getConnected = useCallback((id) => {
259
- const connected = new Set([id]);
260
- const byId = new Map();
261
- positioned.forEach(n => byId.set(n.agentId, n));
262
- // Walk up
263
- let cur = byId.get(id);
264
- while (cur && cur.managerId && byId.has(cur.managerId)) {
265
- connected.add(cur.managerId);
266
- cur = byId.get(cur.managerId);
267
- }
268
- // Walk down
269
- function addDesc(node) {
270
- node.children.forEach(c => { connected.add(c.agentId); addDesc(c); });
271
- }
272
- const node = byId.get(id);
273
- if (node) addDesc(node);
188
+ useEffect(function() { if (positioned.length > 0) fitToView(); }, [positioned.length]);
189
+
190
+ var getConnected = useCallback(function(id) {
191
+ var connected = new Set([id]);
192
+ var byId = new Map(); positioned.forEach(function(n) { byId.set(n.agentId, n); });
193
+ var cur = byId.get(id);
194
+ while (cur && cur.managerId && byId.has(cur.managerId)) { connected.add(cur.managerId); cur = byId.get(cur.managerId); }
195
+ function addDesc(node) { node.children.forEach(function(c) { connected.add(c.agentId); addDesc(c); }); }
196
+ var node = byId.get(id); if (node) addDesc(node);
274
197
  return connected;
275
198
  }, [positioned]);
276
199
 
277
- const connected = hoveredId ? getConnected(hoveredId) : null;
278
-
279
- // Collect edges
280
- const edges = [];
281
- positioned.forEach(node => {
282
- node.children.forEach(child => {
283
- edges.push({ parent: node, child });
284
- });
285
- });
286
-
287
- // Hovered node for tooltip
288
- const hoveredNode = hoveredId ? positioned.find(n => n.agentId === hoveredId) : null;
200
+ var connected = hoveredId ? getConnected(hoveredId) : null;
201
+ var edges = [];
202
+ positioned.forEach(function(node) { node.children.forEach(function(child) { edges.push({ parent: node, child: child }); }); });
203
+ var hoveredNode = hoveredId ? positioned.find(function(n) { return n.agentId === hoveredId; }) : null;
289
204
 
290
205
  if (loading) return h('div', { style: { padding: 40, textAlign: 'center', color: 'var(--text-muted)' } }, 'Loading organization chart...');
291
206
  if (error) return h('div', { style: { padding: 40, textAlign: 'center', color: 'var(--danger)' } }, 'Error: ' + error);
292
207
  if (positioned.length === 0) return h('div', { style: { padding: 40, textAlign: 'center', color: 'var(--text-muted)' } },
293
- h('div', { style: { fontSize: 48, marginBottom: 16 } }, '\u{1F3E2}'),
208
+ h('div', { style: { width: 48, height: 48, borderRadius: 12, background: 'rgba(99,102,241,0.1)', display: 'flex', alignItems: 'center', justifyContent: 'center', margin: '0 auto 16px', color: ACCENT } }, I.orgChart()),
294
209
  h('div', { style: { fontSize: 18, fontWeight: 600, marginBottom: 8 } }, 'No Organization Hierarchy Yet'),
295
210
  h('div', { style: { color: 'var(--text-secondary)' } }, 'Add agents and configure manager relationships to see the org chart.')
296
211
  );
297
212
 
298
- return h('div', { style: { height: '100%', display: 'flex', flexDirection: 'column', background: BG, borderRadius: 'var(--radius-lg)', overflow: 'hidden' } },
213
+ return h('div', { style: { height: '100%', display: 'flex', flexDirection: 'column', background: 'var(--oc-bg)', borderRadius: 'var(--radius-lg)', overflow: 'hidden' } },
299
214
  // Toolbar
300
- h('div', { style: { display: 'flex', alignItems: 'center', gap: 12, padding: '12px 20px', borderBottom: '1px solid rgba(255,255,255,0.08)', background: 'rgba(0,0,0,0.3)', flexShrink: 0 } },
301
- h('div', { style: { fontWeight: 700, fontSize: 16, color: '#fff', display: 'flex', alignItems: 'center' } }, 'Organization Chart', h(HelpButton, { label: 'Organization Chart' },
302
- h('p', null, 'Visual hierarchy of all agents in your organization. Shows reporting relationships, status, and activity at a glance.'),
303
- h('h4', { style: { marginTop: 16, marginBottom: 8, fontSize: 14 } }, 'Interactions'),
304
- h('ul', { style: { paddingLeft: 20, margin: '4px 0 8px' } },
305
- h('li', null, h('strong', null, 'Hover'), ' Highlights the agent\'s full chain (managers above, reports below) and shows a detail tooltip.'),
306
- h('li', null, h('strong', null, 'Scroll'), ' Zoom in/out.'),
307
- h('li', null, h('strong', null, 'Click & drag'), ' Pan the canvas.'),
308
- h('li', null, h('strong', null, 'Fit'), ' Auto-zoom to fit all agents in view.')
309
- ),
310
- h('h4', { style: { marginTop: 16, marginBottom: 8, fontSize: 14 } }, 'Node badges'),
311
- h('ul', { style: { paddingLeft: 20, margin: '4px 0 8px' } },
312
- h('li', null, h('strong', null, 'MGR'), ' This agent manages other agents.'),
313
- h('li', null, h('strong', null, 'N tasks'), ' — Currently active tasks.'),
314
- h('li', null, h('strong', null, 'N err'), ' — Errors recorded today.')
315
- ),
316
- h('div', { style: { marginTop: 12, padding: 12, background: 'var(--bg-secondary, #1e293b)', borderRadius: 'var(--radius, 8px)', fontSize: 13 } }, h('strong', null, 'Tip: '), 'Purple nodes represent external (human) managers. Configure manager relationships in each agent\'s settings.')
317
- )),
318
- h('div', { style: { color: 'rgba(255,255,255,0.4)', fontSize: 13 } }, positioned.length + ' agents'),
215
+ h('div', { style: { display: 'flex', alignItems: 'center', gap: 10, padding: '10px 16px', borderBottom: '1px solid var(--oc-border)', background: 'var(--oc-toolbar)', flexShrink: 0, flexWrap: 'wrap' } },
216
+ h('div', { style: { fontWeight: 700, fontSize: 14, color: 'var(--oc-text)', display: 'flex', alignItems: 'center', gap: 6 } },
217
+ I.orgChart(), 'Organization Chart',
218
+ h(HelpButton, { label: 'Organization Chart' },
219
+ h('p', null, 'Visual hierarchy of all agents in your organization. Shows reporting relationships, status, and activity at a glance.'),
220
+ h('h4', { style: { marginTop: 16, marginBottom: 8, fontSize: 14 } }, 'Interactions'),
221
+ h('ul', { style: { paddingLeft: 20, margin: '4px 0 8px' } },
222
+ h('li', null, h('strong', null, 'Hover'), ' \u2014 Highlights the agent\'s full chain and shows a detail tooltip.'),
223
+ h('li', null, h('strong', null, 'Scroll'), ' \u2014 Zoom in/out.'),
224
+ h('li', null, h('strong', null, 'Drag'), ' \u2014 Pan the canvas.'),
225
+ h('li', null, h('strong', null, 'Fit'), ' \u2014 Auto-zoom to fit all agents.')
226
+ ),
227
+ h('div', { style: { marginTop: 12, padding: 12, background: 'var(--bg-secondary, #1e293b)', borderRadius: 'var(--radius, 8px)', fontSize: 13 } }, h('strong', null, 'Tip: '), 'Purple nodes represent external (human) managers.')
228
+ )
229
+ ),
230
+ h('div', { style: { color: 'var(--oc-dim)', fontSize: 12 } }, positioned.length + ' agents'),
319
231
  h('div', { style: { flex: 1 } }),
320
- // Legend
321
- legendDot('#22c55e', 'Running'),
322
- legendDot('#6b7394', 'Stopped'),
323
- legendDot('#ef4444', 'Error'),
324
- legendDot('#f59e0b', 'Paused'),
325
- legendDot('#8b5cf6', 'External'),
326
- h('div', { style: { width: 1, height: 16, background: 'rgba(255,255,255,0.12)', margin: '0 4px' } }),
327
- // Zoom controls
328
- h('button', { onClick: () => setZoom(z => Math.min(3, z + 0.2)), style: toolbarBtnStyle }, '+'),
329
- h('div', { style: { color: 'rgba(255,255,255,0.5)', fontSize: 12, minWidth: 40, textAlign: 'center' } }, Math.round(zoom * 100) + '%'),
330
- h('button', { onClick: () => setZoom(z => Math.max(0.15, z - 0.2)), style: toolbarBtnStyle }, '\u2212'),
331
- h('button', { onClick: fitToView, style: { ...toolbarBtnStyle, fontSize: 11, padding: '4px 10px' } }, 'Fit'),
332
- h('button', { onClick: load, style: { ...toolbarBtnStyle, fontSize: 11, padding: '4px 10px' } }, 'Refresh'),
232
+ legendDot('#22c55e', 'Running'), legendDot('#6b7394', 'Stopped'), legendDot('#ef4444', 'Error'), legendDot('#f59e0b', 'Paused'), legendDot('#8b5cf6', 'External'),
233
+ h('div', { style: { width: 1, height: 14, background: 'var(--oc-faint)', margin: '0 4px' } }),
234
+ h('button', { onClick: function() { setZoom(function(z) { return Math.min(3, z + 0.2); }); }, style: toolbarBtnStyle }, '+'),
235
+ h('div', { style: { color: 'var(--oc-dim)', fontSize: 11, minWidth: 36, textAlign: 'center' } }, Math.round(zoom * 100) + '%'),
236
+ h('button', { onClick: function() { setZoom(function(z) { return Math.max(0.15, z - 0.2); }); }, style: toolbarBtnStyle }, '\u2212'),
237
+ h('button', { onClick: fitToView, style: Object.assign({}, toolbarBtnStyle, { fontSize: 11, padding: '4px 10px' }) }, 'Fit'),
238
+ h('button', { onClick: load, style: Object.assign({}, toolbarBtnStyle, { fontSize: 11, padding: '4px 10px' }) }, 'Refresh'),
333
239
  ),
334
240
 
241
+ // Summary metrics
242
+ h(OrgSummary, { nodes: positioned }),
243
+
335
244
  // Canvas
336
245
  h('div', {
337
246
  ref: containerRef,
@@ -339,129 +248,81 @@ export function OrgChartPage() {
339
248
  onMouseDown: handleMouseDown,
340
249
  onWheel: handleWheel,
341
250
  },
342
- h('div', { style: { transform: `translate(${pan.x}px, ${pan.y}px) scale(${zoom})`, transformOrigin: '0 0', position: 'absolute', top: 0, left: 0 } },
251
+ h('div', { style: { transform: 'translate(' + pan.x + 'px, ' + pan.y + 'px) scale(' + zoom + ')', transformOrigin: '0 0', position: 'absolute', top: 0, left: 0 } },
343
252
  // SVG edges
344
253
  h('svg', { width: treeW, height: treeH, style: { position: 'absolute', top: 0, left: 0, pointerEvents: 'none' } },
345
254
  h('defs', null,
346
255
  h('marker', { id: 'arrowhead', markerWidth: 8, markerHeight: 6, refX: 8, refY: 3, orient: 'auto' },
347
- h('polygon', { points: '0 0, 8 3, 0 6', fill: EDGE_COLOR })
256
+ h('polygon', { points: '0 0, 8 3, 0 6', fill: 'var(--oc-edge)' })
348
257
  ),
349
258
  h('marker', { id: 'arrowhead-hl', markerWidth: 8, markerHeight: 6, refX: 8, refY: 3, orient: 'auto' },
350
- h('polygon', { points: '0 0, 8 3, 0 6', fill: EDGE_HIGHLIGHT })
351
- ),
259
+ h('polygon', { points: '0 0, 8 3, 0 6', fill: 'rgba(99,102,241,0.7)' })
260
+ )
352
261
  ),
353
- edges.map((e, i) => {
354
- const isHl = connected && connected.has(e.parent.agentId) && connected.has(e.child.agentId);
355
- const dim = connected && !isHl;
356
- return h('path', {
357
- key: i,
358
- d: edgePath(e.parent, e.child),
359
- stroke: isHl ? EDGE_HIGHLIGHT : dim ? 'rgba(255,255,255,0.06)' : EDGE_COLOR,
360
- strokeWidth: isHl ? 2.5 : 1.5,
361
- fill: 'none',
262
+ edges.map(function(e, i) {
263
+ var isHl = connected && connected.has(e.parent.agentId) && connected.has(e.child.agentId);
264
+ var dim = connected && !isHl;
265
+ return h('path', { key: i, d: edgePath(e.parent, e.child),
266
+ stroke: isHl ? 'rgba(99,102,241,0.7)' : dim ? 'var(--oc-edge-dim)' : 'var(--oc-edge)',
267
+ strokeWidth: isHl ? 2.5 : 1.5, fill: 'none',
362
268
  markerEnd: isHl ? 'url(#arrowhead-hl)' : 'url(#arrowhead)',
363
- style: { transition: 'stroke 0.2s, stroke-width 0.2s, opacity 0.2s', opacity: dim ? 0.3 : 1 },
364
- });
269
+ style: { transition: 'stroke 0.2s, opacity 0.2s', opacity: dim ? 0.3 : 1 } });
365
270
  })
366
271
  ),
367
272
  // Nodes
368
- positioned.map(node => {
369
- const isHovered = hoveredId === node.agentId;
370
- const dim = connected && !connected.has(node.agentId);
371
- const stateColor = node.isExternal ? '#8b5cf6' : (STATE_COLORS[node.state] || '#6b7394');
273
+ positioned.map(function(node) {
274
+ var isHovered = hoveredId === node.agentId;
275
+ var dim = connected && !connected.has(node.agentId);
276
+ var stateColor = node.isExternal ? '#8b5cf6' : (STATE_COLORS[node.state] || '#6b7394');
372
277
  return h('div', {
373
- key: node.agentId,
374
- className: 'org-node',
375
- onMouseEnter: (e) => { setHoveredId(node.agentId); setMousePos({ x: e.clientX, y: e.clientY }); },
376
- onMouseMove: (e) => { if (isHovered) setMousePos({ x: e.clientX, y: e.clientY }); },
377
- onMouseLeave: () => setHoveredId(null),
278
+ key: node.agentId, className: 'org-node',
279
+ onMouseEnter: function(ev) { setHoveredId(node.agentId); setMousePos({ x: ev.clientX, y: ev.clientY }); },
280
+ onMouseMove: function(ev) { if (isHovered) setMousePos({ x: ev.clientX, y: ev.clientY }); },
281
+ onMouseLeave: function() { setHoveredId(null); },
378
282
  style: {
379
- position: 'absolute',
380
- left: node.x,
381
- top: node.y,
382
- width: NODE_W,
383
- height: NODE_H,
384
- background: isHovered ? 'rgba(255,255,255,0.06)' : 'rgba(255,255,255,0.02)',
385
- border: `1.5px solid ${isHovered ? 'rgba(255,255,255,0.3)' : 'rgba(255,255,255,0.12)'}`,
386
- borderRadius: 12,
387
- padding: '10px 14px',
388
- cursor: 'pointer',
389
- transition: 'all 0.2s',
390
- opacity: dim ? 0.2 : 1,
391
- backdropFilter: 'blur(8px)',
392
- display: 'flex',
393
- alignItems: 'center',
394
- gap: 10,
395
- userSelect: 'none',
283
+ position: 'absolute', left: node.x, top: node.y, width: NODE_W, height: NODE_H,
284
+ background: isHovered ? 'var(--oc-card-h)' : 'var(--oc-card)',
285
+ border: '1.5px solid ' + (isHovered ? stateColor + '66' : 'var(--oc-faint)'),
286
+ borderRadius: 12, padding: '10px 14px', cursor: 'pointer',
287
+ transition: 'all 0.2s', opacity: dim ? 0.2 : 1, backdropFilter: 'blur(8px)',
288
+ display: 'flex', alignItems: 'center', gap: 10, userSelect: 'none',
396
289
  },
397
290
  },
398
- // Status dot + avatar
399
291
  h('div', { style: { position: 'relative', flexShrink: 0 } },
400
292
  node.avatar
401
- ? h('img', { src: node.avatar, style: {
402
- width: 36, height: 36, borderRadius: '50%',
403
- border: `2px solid ${stateColor}`,
404
- objectFit: 'cover',
405
- }})
406
- : h('div', { style: {
407
- width: 36, height: 36, borderRadius: '50%',
408
- background: node.isExternal ? 'linear-gradient(135deg, #7c3aed, #a78bfa)' : `linear-gradient(135deg, ${stateColor}33, ${stateColor}11)`,
409
- border: `2px solid ${stateColor}`,
410
- display: 'flex', alignItems: 'center', justifyContent: 'center',
411
- fontSize: 14, fontWeight: 700, color: node.isExternal ? '#fff' : stateColor,
412
- }}, (node.name || '?').charAt(0).toUpperCase()),
413
- // Online indicator
414
- h('div', { style: {
415
- position: 'absolute', bottom: -1, right: -1,
416
- width: 10, height: 10, borderRadius: '50%',
417
- background: stateColor,
418
- border: '2px solid ' + BG,
419
- }}),
293
+ ? h('img', { src: node.avatar, style: { width: 36, height: 36, borderRadius: '50%', border: '2px solid ' + stateColor, objectFit: 'cover' } })
294
+ : h('div', { style: { width: 36, height: 36, borderRadius: '50%', background: node.isExternal ? 'linear-gradient(135deg, #7c3aed, #a78bfa)' : stateColor + '22', border: '2px solid ' + stateColor, display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 14, fontWeight: 700, color: node.isExternal ? '#fff' : stateColor } }, (node.name || '?').charAt(0).toUpperCase()),
295
+ h('div', { style: { position: 'absolute', bottom: -1, right: -1, width: 10, height: 10, borderRadius: '50%', background: stateColor, border: '2px solid var(--oc-bg)' } })
420
296
  ),
421
- // Text
422
297
  h('div', { style: { overflow: 'hidden', flex: 1, minWidth: 0 } },
423
- h('div', { style: { fontSize: 13, fontWeight: 600, color: '#fff', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' } }, node.name || node.agentId),
424
- h('div', { style: { fontSize: 11, color: 'rgba(255,255,255,0.45)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', marginTop: 2 } },
425
- node.role || (node.isExternal ? 'External Manager' : 'Agent')
426
- ),
427
- // Tags row
298
+ h('div', { style: { fontSize: 13, fontWeight: 600, color: 'var(--oc-text)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' } }, node.name || node.agentId),
299
+ h('div', { style: { fontSize: 11, color: 'var(--oc-dim)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', marginTop: 2 } }, node.role || (node.isExternal ? 'External Manager' : 'Agent')),
428
300
  !node.isExternal && h('div', { style: { display: 'flex', gap: 4, marginTop: 4 } },
429
- node.isManager && h('span', { style: tagStyle('#6366f1') }, 'MGR'),
301
+ node.isManager && h('span', { style: tagStyle(ACCENT) }, 'MGR'),
430
302
  node.activeTasks > 0 && h('span', { style: tagStyle('#f59e0b') }, node.activeTasks + ' tasks'),
431
- node.errorsToday > 0 && h('span', { style: tagStyle('#ef4444') }, node.errorsToday + ' err'),
432
- ),
433
- ),
303
+ node.errorsToday > 0 && h('span', { style: tagStyle('#ef4444') }, node.errorsToday + ' err')
304
+ )
305
+ )
434
306
  );
435
- }),
436
- ),
307
+ })
308
+ )
437
309
  ),
438
310
 
439
311
  // Hover tooltip
440
312
  hoveredNode && h('div', { style: {
441
- position: 'fixed',
442
- left: mousePos.x + 16,
443
- top: mousePos.y - 10,
444
- background: 'rgba(15,17,23,0.95)',
445
- backdropFilter: 'blur(12px)',
446
- border: '1px solid rgba(255,255,255,0.15)',
447
- borderRadius: 10,
448
- padding: '12px 16px',
449
- pointerEvents: 'none',
450
- zIndex: 1000,
451
- minWidth: 200,
452
- maxWidth: 280,
313
+ position: 'fixed', left: mousePos.x + 16, top: mousePos.y - 10,
314
+ background: 'var(--oc-tip-bg)', backdropFilter: 'blur(12px)',
315
+ border: '1px solid var(--oc-faint)', borderRadius: 10,
316
+ padding: '12px 16px', pointerEvents: 'none', zIndex: 1000, minWidth: 200, maxWidth: 280,
453
317
  }},
454
- // Header with avatar
455
318
  h('div', { style: { display: 'flex', alignItems: 'center', gap: 10, marginBottom: 10 } },
456
319
  hoveredNode.avatar
457
320
  ? h('img', { src: hoveredNode.avatar, style: { width: 32, height: 32, borderRadius: '50%', border: '2px solid ' + (STATE_COLORS[hoveredNode.state] || '#6b7394'), objectFit: 'cover' } })
458
- : h('div', { style: { width: 32, height: 32, borderRadius: '50%', background: 'rgba(255,255,255,0.1)', border: '2px solid ' + (STATE_COLORS[hoveredNode.state] || '#6b7394'), display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 13, fontWeight: 700, color: '#fff' } }, (hoveredNode.name || '?').charAt(0).toUpperCase()),
321
+ : h('div', { style: { width: 32, height: 32, borderRadius: '50%', background: 'var(--oc-card-h)', border: '2px solid ' + (STATE_COLORS[hoveredNode.state] || '#6b7394'), display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 13, fontWeight: 700, color: 'var(--oc-text)' } }, (hoveredNode.name || '?').charAt(0).toUpperCase()),
459
322
  h('div', null,
460
- h('div', { style: { fontSize: 13, fontWeight: 600, color: '#fff' } }, hoveredNode.name),
461
- h('div', { style: { fontSize: 11, color: 'rgba(255,255,255,0.4)' } }, hoveredNode.role),
462
- ),
323
+ h('div', { style: { fontSize: 13, fontWeight: 600, color: 'var(--oc-text)' } }, hoveredNode.name),
324
+ h('div', { style: { fontSize: 11, color: 'var(--oc-dim)' } }, hoveredNode.role))
463
325
  ),
464
- // Info rows
465
326
  h('div', { style: { display: 'flex', flexDirection: 'column', gap: 4 } },
466
327
  tooltipRow('State', hoveredNode.state || 'unknown', STATE_COLORS[hoveredNode.state]),
467
328
  !hoveredNode.isExternal && tooltipRow('Clocked In', hoveredNode.clockedIn ? 'Yes' : 'No', hoveredNode.clockedIn ? '#22c55e' : '#6b7394'),
@@ -470,59 +331,8 @@ export function OrgChartPage() {
470
331
  hoveredNode.subordinateCount > 0 && tooltipRow('Direct Reports', String(hoveredNode.subordinateCount), ACCENT),
471
332
  hoveredNode.activeTasks > 0 && tooltipRow('Active Tasks', String(hoveredNode.activeTasks), '#f59e0b'),
472
333
  hoveredNode.errorsToday > 0 && tooltipRow('Errors Today', String(hoveredNode.errorsToday), '#ef4444'),
473
- hoveredNode.lastActivityAt && tooltipRow('Last Active', timeAgo(hoveredNode.lastActivityAt)),
474
- ),
475
- ),
476
-
477
- // (legend moved to toolbar)
478
- );
479
- }
480
-
481
- // ─── Helpers ─────────────────────────────────────────────
482
- const toolbarBtnStyle = {
483
- background: 'rgba(255,255,255,0.08)',
484
- border: '1px solid rgba(255,255,255,0.12)',
485
- borderRadius: 6,
486
- color: '#fff',
487
- fontSize: 14,
488
- fontWeight: 600,
489
- padding: '4px 8px',
490
- cursor: 'pointer',
491
- lineHeight: '1.2',
492
- };
493
-
494
- function tagStyle(color) {
495
- return {
496
- fontSize: 9,
497
- fontWeight: 600,
498
- padding: '1px 5px',
499
- borderRadius: 4,
500
- background: color + '22',
501
- color: color,
502
- letterSpacing: '0.02em',
503
- };
504
- }
505
-
506
- function tooltipRow(label, value, color) {
507
- return h('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', fontSize: 11 } },
508
- h('span', { style: { color: 'rgba(255,255,255,0.4)' } }, label),
509
- h('span', { style: { fontWeight: 600, color: color || '#fff' } }, value),
334
+ hoveredNode.lastActivityAt && tooltipRow('Last Active', timeAgo(hoveredNode.lastActivityAt))
335
+ )
336
+ )
510
337
  );
511
338
  }
512
-
513
- function legendDot(color, label) {
514
- return h('div', { style: { display: 'flex', alignItems: 'center', gap: 4 } },
515
- h('div', { style: { width: 8, height: 8, borderRadius: '50%', background: color } }),
516
- h('span', { style: { color: 'rgba(255,255,255,0.5)' } }, label),
517
- );
518
- }
519
-
520
- function timeAgo(iso) {
521
- const diff = Date.now() - new Date(iso).getTime();
522
- const mins = Math.floor(diff / 60000);
523
- if (mins < 1) return 'Just now';
524
- if (mins < 60) return mins + 'm ago';
525
- const hrs = Math.floor(mins / 60);
526
- if (hrs < 24) return hrs + 'h ago';
527
- return Math.floor(hrs / 24) + 'd ago';
528
- }