claude-team-dashboard 1.2.2
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.
Potentially problematic release.
This version of claude-team-dashboard might be problematic. Click here for more details.
- package/CHANGELOG.md +76 -0
- package/LICENSE +21 -0
- package/README.md +722 -0
- package/cleanup.js +73 -0
- package/config.js +50 -0
- package/dist/assets/icons-Ijf8rQIc.js +1 -0
- package/dist/assets/index-Cqc1m1x_.css +1 -0
- package/dist/assets/index-jGy3ms0W.js +9 -0
- package/dist/assets/react-vendor-DbmSkCAF.js +1 -0
- package/dist/index.html +16 -0
- package/index.html +13 -0
- package/package.json +93 -0
- package/server.js +953 -0
- package/src/App.jsx +372 -0
- package/src/animations-enhanced.css +929 -0
- package/src/animations.css +783 -0
- package/src/components/ActivityFeed.jsx +289 -0
- package/src/components/AgentActivity.jsx +104 -0
- package/src/components/AgentCard.jsx +163 -0
- package/src/components/AgentOutputViewer.jsx +334 -0
- package/src/components/ArchiveViewer.jsx +283 -0
- package/src/components/ConnectionStatus.jsx +124 -0
- package/src/components/DetailedTaskProgress.jsx +126 -0
- package/src/components/ErrorBoundary.jsx +132 -0
- package/src/components/Header.jsx +154 -0
- package/src/components/LiveAgentStream.jsx +176 -0
- package/src/components/LiveCommunication.jsx +326 -0
- package/src/components/LiveMetrics.jsx +100 -0
- package/src/components/RealTimeMessages.jsx +298 -0
- package/src/components/SkeletonLoader.jsx +384 -0
- package/src/components/StatsOverview.jsx +209 -0
- package/src/components/SystemStatus.jsx +57 -0
- package/src/components/TaskList.jsx +306 -0
- package/src/components/TeamCard.jsx +126 -0
- package/src/components/TeamHistory.jsx +204 -0
- package/src/components/__tests__/ConnectionStatus.test.jsx +54 -0
- package/src/components/__tests__/StatsOverview.test.jsx +66 -0
- package/src/config/constants.js +59 -0
- package/src/hooks/useCounterAnimation.js +219 -0
- package/src/hooks/useWebSocket.js +76 -0
- package/src/index.css +1818 -0
- package/src/main.jsx +17 -0
- package/src/polish-enhancements.css +303 -0
- package/src/premium-visual-polish.css +830 -0
- package/src/responsive-enhancements.css +666 -0
- package/src/styles/theme.css +395 -0
- package/src/test/setup.js +19 -0
- package/start.js +36 -0
- package/vite.config.js +37 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import PropTypes from 'prop-types';
|
|
3
|
+
import { Users, ChevronDown, ChevronUp, Activity, Clock, CheckCircle, Loader } from 'lucide-react';
|
|
4
|
+
import { AgentCard } from './AgentCard';
|
|
5
|
+
import { TaskList } from './TaskList';
|
|
6
|
+
import dayjs from 'dayjs';
|
|
7
|
+
import relativeTime from 'dayjs/plugin/relativeTime';
|
|
8
|
+
dayjs.extend(relativeTime);
|
|
9
|
+
|
|
10
|
+
export function TeamCard({ team }) {
|
|
11
|
+
const [isExpanded, setIsExpanded] = useState(true);
|
|
12
|
+
|
|
13
|
+
const { name, config, tasks, lastUpdated } = team;
|
|
14
|
+
const members = config.members || [];
|
|
15
|
+
const lead = members.find(m => m.name === config.leadName);
|
|
16
|
+
|
|
17
|
+
const taskStats = {
|
|
18
|
+
pending: tasks.filter(t => t.status === 'pending').length,
|
|
19
|
+
inProgress: tasks.filter(t => t.status === 'in_progress').length,
|
|
20
|
+
completed: tasks.filter(t => t.status === 'completed').length
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<div className="card border-l-4 border-l-claude-orange">
|
|
25
|
+
<div className="flex items-center justify-between mb-4">
|
|
26
|
+
<div className="flex items-center gap-3">
|
|
27
|
+
<div className="bg-claude-orange/20 p-2 rounded-lg">
|
|
28
|
+
<Users className="h-6 w-6 text-claude-orange" />
|
|
29
|
+
</div>
|
|
30
|
+
<div>
|
|
31
|
+
<h3 className="text-xl font-bold text-white">{name}</h3>
|
|
32
|
+
{config.description && (
|
|
33
|
+
<p className="text-gray-400 text-sm mt-1">{config.description}</p>
|
|
34
|
+
)}
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
<button
|
|
38
|
+
onClick={() => setIsExpanded(!isExpanded)}
|
|
39
|
+
className="p-2 hover:bg-gray-700 rounded-lg transition-colors"
|
|
40
|
+
aria-label={isExpanded ? "Collapse team details" : "Expand team details"}
|
|
41
|
+
aria-expanded={isExpanded}
|
|
42
|
+
>
|
|
43
|
+
{isExpanded ? (
|
|
44
|
+
<ChevronUp className="h-5 w-5 text-gray-400" />
|
|
45
|
+
) : (
|
|
46
|
+
<ChevronDown className="h-5 w-5 text-gray-400" />
|
|
47
|
+
)}
|
|
48
|
+
</button>
|
|
49
|
+
</div>
|
|
50
|
+
|
|
51
|
+
<div className="flex flex-wrap gap-2 mb-4">
|
|
52
|
+
<div className="flex items-center gap-2 bg-gray-700/50 px-3 py-1 rounded-full">
|
|
53
|
+
<Users className="h-4 w-4 text-gray-400" />
|
|
54
|
+
<span className="text-sm text-gray-300">{members.length} agents</span>
|
|
55
|
+
</div>
|
|
56
|
+
<div className="flex items-center gap-2 bg-gray-700/50 px-3 py-1 rounded-full">
|
|
57
|
+
<Activity className="h-4 w-4 text-gray-400" />
|
|
58
|
+
<span className="text-sm text-gray-300">{tasks.length} tasks</span>
|
|
59
|
+
</div>
|
|
60
|
+
<div className="flex items-center gap-2 bg-gray-700/50 px-3 py-1 rounded-full">
|
|
61
|
+
<Clock className="h-4 w-4 text-gray-400" />
|
|
62
|
+
<span className="text-sm text-gray-300">
|
|
63
|
+
{dayjs(new Date(lastUpdated)).fromNow()}
|
|
64
|
+
</span>
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
|
|
68
|
+
<div className="flex gap-2 mb-4">
|
|
69
|
+
<span className="badge badge-pending">
|
|
70
|
+
<Clock className="h-3 w-3 inline-block mr-1" aria-hidden="true" />
|
|
71
|
+
{taskStats.pending} pending
|
|
72
|
+
</span>
|
|
73
|
+
<span className="badge badge-in-progress">
|
|
74
|
+
<Loader className="h-3 w-3 inline-block mr-1 animate-spin" aria-hidden="true" />
|
|
75
|
+
{taskStats.inProgress} in progress
|
|
76
|
+
</span>
|
|
77
|
+
<span className="badge badge-completed">
|
|
78
|
+
<CheckCircle className="h-3 w-3 inline-block mr-1" aria-hidden="true" />
|
|
79
|
+
{taskStats.completed} completed
|
|
80
|
+
</span>
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
{isExpanded && (
|
|
84
|
+
<div className="space-y-6 mt-6">
|
|
85
|
+
<div>
|
|
86
|
+
<h4 className="text-lg font-semibold text-white mb-3 flex items-center gap-2">
|
|
87
|
+
<Users className="h-5 w-5" />
|
|
88
|
+
Team Members
|
|
89
|
+
</h4>
|
|
90
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
|
91
|
+
{lead && (
|
|
92
|
+
<AgentCard agent={lead} isLead={true} />
|
|
93
|
+
)}
|
|
94
|
+
{members
|
|
95
|
+
.filter(m => m.name !== config.leadName)
|
|
96
|
+
.map((agent, index) => (
|
|
97
|
+
<AgentCard key={index} agent={agent} isLead={false} />
|
|
98
|
+
))}
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
<div>
|
|
103
|
+
<h4 className="text-lg font-semibold text-white mb-3 flex items-center gap-2">
|
|
104
|
+
<Activity className="h-5 w-5" />
|
|
105
|
+
Tasks
|
|
106
|
+
</h4>
|
|
107
|
+
<TaskList tasks={tasks} />
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
)}
|
|
111
|
+
</div>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
TeamCard.propTypes = {
|
|
116
|
+
team: PropTypes.shape({
|
|
117
|
+
name: PropTypes.string.isRequired,
|
|
118
|
+
config: PropTypes.shape({
|
|
119
|
+
description: PropTypes.string,
|
|
120
|
+
leadName: PropTypes.string,
|
|
121
|
+
members: PropTypes.arrayOf(PropTypes.object)
|
|
122
|
+
}),
|
|
123
|
+
tasks: PropTypes.array,
|
|
124
|
+
lastUpdated: PropTypes.string
|
|
125
|
+
}).isRequired
|
|
126
|
+
};
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { History, Users, CheckCircle2, Clock, ChevronDown, ChevronRight } from 'lucide-react';
|
|
3
|
+
import dayjs from 'dayjs';
|
|
4
|
+
import relativeTime from 'dayjs/plugin/relativeTime';
|
|
5
|
+
dayjs.extend(relativeTime);
|
|
6
|
+
|
|
7
|
+
export function TeamHistory({ teamHistory }) {
|
|
8
|
+
const [expandedTeam, setExpandedTeam] = useState(null);
|
|
9
|
+
|
|
10
|
+
if (!teamHistory || teamHistory.length === 0) {
|
|
11
|
+
return (
|
|
12
|
+
<div className="card">
|
|
13
|
+
<div className="flex items-center gap-2 mb-4">
|
|
14
|
+
<History className="h-5 w-5 text-claude-orange" />
|
|
15
|
+
<h3 className="text-lg font-semibold text-white">Team History</h3>
|
|
16
|
+
</div>
|
|
17
|
+
<div className="text-center py-12 text-gray-400">
|
|
18
|
+
<History className="h-16 w-16 mx-auto mb-3 opacity-50" />
|
|
19
|
+
<p className="text-sm">No team history yet</p>
|
|
20
|
+
<p className="text-xs mt-1">Past teams will appear here</p>
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const toggleTeam = (teamName) => {
|
|
27
|
+
setExpandedTeam(expandedTeam === teamName ? null : teamName);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const getTaskStats = (tasks) => {
|
|
31
|
+
return {
|
|
32
|
+
total: tasks.length,
|
|
33
|
+
completed: tasks.filter(t => t.status === 'completed').length,
|
|
34
|
+
inProgress: tasks.filter(t => t.status === 'in_progress').length,
|
|
35
|
+
pending: tasks.filter(t => t.status === 'pending').length
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<div className="card">
|
|
41
|
+
<div className="flex items-center justify-between mb-4">
|
|
42
|
+
<div className="flex items-center gap-2">
|
|
43
|
+
<History className="h-5 w-5 text-claude-orange" />
|
|
44
|
+
<h3 className="text-lg font-semibold text-white">Team History</h3>
|
|
45
|
+
</div>
|
|
46
|
+
<span className="text-sm text-gray-400">
|
|
47
|
+
{teamHistory.length} team{teamHistory.length !== 1 ? 's' : ''}
|
|
48
|
+
</span>
|
|
49
|
+
</div>
|
|
50
|
+
|
|
51
|
+
<div className="space-y-3 max-h-[600px] overflow-y-auto">
|
|
52
|
+
{teamHistory.map((team, index) => {
|
|
53
|
+
const stats = getTaskStats(team.tasks);
|
|
54
|
+
const isExpanded = expandedTeam === team.name;
|
|
55
|
+
const completionRate = stats.total > 0 ? (stats.completed / stats.total) * 100 : 0;
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<div
|
|
59
|
+
key={team.name}
|
|
60
|
+
className="border border-gray-700 rounded-xl overflow-hidden transition-all hover:border-claude-orange/50"
|
|
61
|
+
style={{
|
|
62
|
+
animation: `fadeInScale 0.3s ease-out ${index * 0.05}s backwards`
|
|
63
|
+
}}
|
|
64
|
+
>
|
|
65
|
+
{/* Team Header */}
|
|
66
|
+
<div
|
|
67
|
+
className="p-4 bg-gray-700/30 cursor-pointer hover:bg-gray-700/50 transition-colors"
|
|
68
|
+
onClick={() => toggleTeam(team.name)}
|
|
69
|
+
role="button"
|
|
70
|
+
tabIndex={0}
|
|
71
|
+
aria-expanded={isExpanded}
|
|
72
|
+
aria-label={isExpanded ? `Collapse ${team.name} details` : `Expand ${team.name} details`}
|
|
73
|
+
onKeyDown={(e) => {
|
|
74
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
75
|
+
e.preventDefault();
|
|
76
|
+
toggleTeam(team.name);
|
|
77
|
+
}
|
|
78
|
+
}}
|
|
79
|
+
>
|
|
80
|
+
<div className="flex items-start gap-3">
|
|
81
|
+
<div className="mt-1" aria-hidden="true">
|
|
82
|
+
{isExpanded ? (
|
|
83
|
+
<ChevronDown className="h-5 w-5 text-gray-400" />
|
|
84
|
+
) : (
|
|
85
|
+
<ChevronRight className="h-5 w-5 text-gray-400" />
|
|
86
|
+
)}
|
|
87
|
+
</div>
|
|
88
|
+
|
|
89
|
+
<div className="flex-1 min-w-0">
|
|
90
|
+
<div className="flex items-center gap-2 mb-1">
|
|
91
|
+
<h4 className="text-white font-semibold">{team.name}</h4>
|
|
92
|
+
{team.isActive && (
|
|
93
|
+
<span className="px-2 py-0.5 bg-green-500/20 text-green-400 text-xs rounded-full border border-green-500/30">
|
|
94
|
+
Active
|
|
95
|
+
</span>
|
|
96
|
+
)}
|
|
97
|
+
</div>
|
|
98
|
+
|
|
99
|
+
<div className="flex items-center gap-4 text-xs text-gray-400 mb-2">
|
|
100
|
+
<div className="flex items-center gap-1">
|
|
101
|
+
<Users className="h-3 w-3" />
|
|
102
|
+
{team.config.members?.length || 0} members
|
|
103
|
+
</div>
|
|
104
|
+
<div className="flex items-center gap-1">
|
|
105
|
+
<CheckCircle2 className="h-3 w-3" />
|
|
106
|
+
{stats.total} tasks
|
|
107
|
+
</div>
|
|
108
|
+
<div className="flex items-center gap-1">
|
|
109
|
+
<Clock className="h-3 w-3" />
|
|
110
|
+
{dayjs(team.lastModified).fromNow()}
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
|
|
114
|
+
{/* Progress Bar */}
|
|
115
|
+
<div className="w-full bg-gray-600 rounded-full h-2">
|
|
116
|
+
<div
|
|
117
|
+
className="bg-gradient-to-r from-green-500 to-emerald-400 h-2 rounded-full transition-all duration-500"
|
|
118
|
+
style={{ width: `${completionRate}%` }}
|
|
119
|
+
></div>
|
|
120
|
+
</div>
|
|
121
|
+
<div className="mt-1 text-xs text-gray-400">
|
|
122
|
+
{stats.completed}/{stats.total} tasks completed ({Math.round(completionRate)}%)
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
|
|
128
|
+
{/* Expanded Content */}
|
|
129
|
+
{isExpanded && (
|
|
130
|
+
<div className="p-4 bg-gray-800/30 border-t border-gray-700 space-y-4">
|
|
131
|
+
{/* Members */}
|
|
132
|
+
<div>
|
|
133
|
+
<h5 className="text-sm font-semibold text-white mb-2">Team Members</h5>
|
|
134
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
|
|
135
|
+
{team.config.members?.map((member, idx) => (
|
|
136
|
+
<div
|
|
137
|
+
key={idx}
|
|
138
|
+
className="p-2 rounded-lg bg-gray-700/30 border border-gray-600/50"
|
|
139
|
+
>
|
|
140
|
+
<div className="font-medium text-sm text-white">{member.name}</div>
|
|
141
|
+
<div className="text-xs text-gray-400">{member.agentType}</div>
|
|
142
|
+
</div>
|
|
143
|
+
))}
|
|
144
|
+
</div>
|
|
145
|
+
</div>
|
|
146
|
+
|
|
147
|
+
{/* Task Stats */}
|
|
148
|
+
<div>
|
|
149
|
+
<h5 className="text-sm font-semibold text-white mb-2">Task Summary</h5>
|
|
150
|
+
<div className="grid grid-cols-4 gap-2">
|
|
151
|
+
<div className="p-2 rounded-lg bg-gray-700/30 text-center">
|
|
152
|
+
<div className="text-lg font-bold text-white">{stats.total}</div>
|
|
153
|
+
<div className="text-xs text-gray-400">Total</div>
|
|
154
|
+
</div>
|
|
155
|
+
<div className="p-2 rounded-lg bg-green-500/10 text-center">
|
|
156
|
+
<div className="text-lg font-bold text-green-400">{stats.completed}</div>
|
|
157
|
+
<div className="text-xs text-gray-400">Done</div>
|
|
158
|
+
</div>
|
|
159
|
+
<div className="p-2 rounded-lg bg-blue-500/10 text-center">
|
|
160
|
+
<div className="text-lg font-bold text-blue-400">{stats.inProgress}</div>
|
|
161
|
+
<div className="text-xs text-gray-400">Active</div>
|
|
162
|
+
</div>
|
|
163
|
+
<div className="p-2 rounded-lg bg-yellow-500/10 text-center">
|
|
164
|
+
<div className="text-lg font-bold text-yellow-400">{stats.pending}</div>
|
|
165
|
+
<div className="text-xs text-gray-400">Pending</div>
|
|
166
|
+
</div>
|
|
167
|
+
</div>
|
|
168
|
+
</div>
|
|
169
|
+
|
|
170
|
+
{/* Recent Tasks */}
|
|
171
|
+
{team.tasks.slice(0, 5).length > 0 && (
|
|
172
|
+
<div>
|
|
173
|
+
<h5 className="text-sm font-semibold text-white mb-2">Recent Tasks</h5>
|
|
174
|
+
<div className="space-y-1">
|
|
175
|
+
{team.tasks.slice(0, 5).map((task, idx) => (
|
|
176
|
+
<div
|
|
177
|
+
key={idx}
|
|
178
|
+
className="p-2 rounded-lg bg-gray-700/20 text-sm"
|
|
179
|
+
>
|
|
180
|
+
<div className="flex items-center gap-2">
|
|
181
|
+
<span className={`h-2 w-2 rounded-full flex-shrink-0 ${
|
|
182
|
+
task.status === 'completed' ? 'bg-green-400' :
|
|
183
|
+
task.status === 'in_progress' ? 'bg-blue-400' :
|
|
184
|
+
'bg-yellow-400'
|
|
185
|
+
}`}></span>
|
|
186
|
+
<span className="text-white truncate">{task.subject}</span>
|
|
187
|
+
{task.owner && (
|
|
188
|
+
<span className="ml-auto text-xs text-gray-400">{task.owner}</span>
|
|
189
|
+
)}
|
|
190
|
+
</div>
|
|
191
|
+
</div>
|
|
192
|
+
))}
|
|
193
|
+
</div>
|
|
194
|
+
</div>
|
|
195
|
+
)}
|
|
196
|
+
</div>
|
|
197
|
+
)}
|
|
198
|
+
</div>
|
|
199
|
+
);
|
|
200
|
+
})}
|
|
201
|
+
</div>
|
|
202
|
+
</div>
|
|
203
|
+
);
|
|
204
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { render, screen } from '@testing-library/react';
|
|
3
|
+
import { ConnectionStatus } from '../ConnectionStatus';
|
|
4
|
+
|
|
5
|
+
describe('ConnectionStatus Component', () => {
|
|
6
|
+
it('renders connected status correctly', () => {
|
|
7
|
+
render(<ConnectionStatus isConnected={true} error={null} />);
|
|
8
|
+
|
|
9
|
+
const statusText = screen.getByText(/connected/i);
|
|
10
|
+
expect(statusText).toBeInTheDocument();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('renders connecting status when disconnected without error', () => {
|
|
14
|
+
render(<ConnectionStatus isConnected={false} error={null} />);
|
|
15
|
+
|
|
16
|
+
const statusText = screen.getByText(/connecting/i);
|
|
17
|
+
expect(statusText).toBeInTheDocument();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('renders error message when error provided', () => {
|
|
21
|
+
const errorMessage = 'Connection failed';
|
|
22
|
+
render(<ConnectionStatus isConnected={false} error={errorMessage} />);
|
|
23
|
+
|
|
24
|
+
const errorText = screen.getByText(errorMessage);
|
|
25
|
+
expect(errorText).toBeInTheDocument();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('displays green indicator when connected', () => {
|
|
29
|
+
const { container } = render(<ConnectionStatus isConnected={true} error={null} />);
|
|
30
|
+
|
|
31
|
+
// Check for green color classes
|
|
32
|
+
const statusDiv = container.querySelector('.bg-green-500\\/20');
|
|
33
|
+
expect(statusDiv).toBeInTheDocument();
|
|
34
|
+
expect(statusDiv).toHaveClass('text-green-400');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('displays red indicator when error occurs', () => {
|
|
38
|
+
const { container } = render(<ConnectionStatus isConnected={false} error="Error" />);
|
|
39
|
+
|
|
40
|
+
// Check for red color classes
|
|
41
|
+
const statusDiv = container.querySelector('.bg-red-500\\/20');
|
|
42
|
+
expect(statusDiv).toBeInTheDocument();
|
|
43
|
+
expect(statusDiv).toHaveClass('text-red-400');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('displays yellow indicator when connecting', () => {
|
|
47
|
+
const { container } = render(<ConnectionStatus isConnected={false} error={null} />);
|
|
48
|
+
|
|
49
|
+
// Check for yellow color classes
|
|
50
|
+
const statusDiv = container.querySelector('.bg-yellow-500\\/20');
|
|
51
|
+
expect(statusDiv).toBeInTheDocument();
|
|
52
|
+
expect(statusDiv).toHaveClass('text-yellow-400');
|
|
53
|
+
});
|
|
54
|
+
});
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { render, screen } from '@testing-library/react';
|
|
3
|
+
import { StatsOverview } from '../StatsOverview';
|
|
4
|
+
|
|
5
|
+
describe('StatsOverview Component', () => {
|
|
6
|
+
const mockStats = {
|
|
7
|
+
totalTeams: 4,
|
|
8
|
+
totalAgents: 19,
|
|
9
|
+
totalTasks: 42,
|
|
10
|
+
inProgressTasks: 18,
|
|
11
|
+
completedTasks: 16,
|
|
12
|
+
blockedTasks: 2
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
it('renders all stats correctly', () => {
|
|
16
|
+
render(<StatsOverview stats={mockStats} />);
|
|
17
|
+
|
|
18
|
+
expect(screen.getByText('4')).toBeInTheDocument();
|
|
19
|
+
expect(screen.getByText('19')).toBeInTheDocument();
|
|
20
|
+
expect(screen.getByText('42')).toBeInTheDocument();
|
|
21
|
+
expect(screen.getByText('18')).toBeInTheDocument();
|
|
22
|
+
expect(screen.getByText('16')).toBeInTheDocument();
|
|
23
|
+
expect(screen.getByText('2')).toBeInTheDocument();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('renders nothing when no stats provided', () => {
|
|
27
|
+
const { container } = render(<StatsOverview stats={null} />);
|
|
28
|
+
|
|
29
|
+
// Component returns null when stats is null
|
|
30
|
+
expect(container.firstChild).toBeNull();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('renders all stat labels correctly', () => {
|
|
34
|
+
render(<StatsOverview stats={mockStats} />);
|
|
35
|
+
|
|
36
|
+
expect(screen.getByText('Active Teams')).toBeInTheDocument();
|
|
37
|
+
expect(screen.getByText('Total Agents')).toBeInTheDocument();
|
|
38
|
+
expect(screen.getByText('Total Tasks')).toBeInTheDocument();
|
|
39
|
+
expect(screen.getByText('In Progress')).toBeInTheDocument();
|
|
40
|
+
expect(screen.getByText('Completed')).toBeInTheDocument();
|
|
41
|
+
expect(screen.getByText('Blocked')).toBeInTheDocument();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('renders with correct grid layout', () => {
|
|
45
|
+
const { container } = render(<StatsOverview stats={mockStats} />);
|
|
46
|
+
|
|
47
|
+
const grid = container.querySelector('.grid');
|
|
48
|
+
expect(grid).toHaveClass('grid-cols-2', 'md:grid-cols-3', 'lg:grid-cols-6');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('renders icons for each stat', () => {
|
|
52
|
+
const { container } = render(<StatsOverview stats={mockStats} />);
|
|
53
|
+
|
|
54
|
+
// Check for 6 icon containers (one for each stat)
|
|
55
|
+
const iconContainers = container.querySelectorAll('.flex-shrink-0');
|
|
56
|
+
expect(iconContainers.length).toBe(6);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('displays stat values with correct formatting', () => {
|
|
60
|
+
render(<StatsOverview stats={mockStats} />);
|
|
61
|
+
|
|
62
|
+
// Check that values are displayed as bold white text
|
|
63
|
+
const valueElements = screen.getByText('4').closest('p');
|
|
64
|
+
expect(valueElements).toHaveClass('text-xl', 'font-bold', 'text-white');
|
|
65
|
+
});
|
|
66
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// WebSocket Configuration
|
|
2
|
+
export const WS_CONFIG = {
|
|
3
|
+
RECONNECT_DELAY: 3000,
|
|
4
|
+
PING_INTERVAL: 30000,
|
|
5
|
+
MAX_RECONNECT_ATTEMPTS: 5
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
// Server Configuration
|
|
9
|
+
export const SERVER_CONFIG = {
|
|
10
|
+
WS_PORT: 3001,
|
|
11
|
+
API_PORT: 3001,
|
|
12
|
+
ALLOWED_ORIGINS: ['http://localhost:5173', 'http://127.0.0.1:5173']
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
// UI Configuration
|
|
16
|
+
export const UI_CONFIG = {
|
|
17
|
+
MAX_ACTIVITY_ITEMS: 50,
|
|
18
|
+
ACTIVITY_UPDATE_INTERVAL: 5000,
|
|
19
|
+
PULSE_ANIMATION_DURATION: 300,
|
|
20
|
+
DEFAULT_TEAM_EXPANDED: true
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// Task Status
|
|
24
|
+
export const TASK_STATUS = {
|
|
25
|
+
PENDING: 'pending',
|
|
26
|
+
IN_PROGRESS: 'in_progress',
|
|
27
|
+
COMPLETED: 'completed',
|
|
28
|
+
DELETED: 'deleted'
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// Theme Colors
|
|
32
|
+
export const THEME_COLORS = {
|
|
33
|
+
CLAUDE_ORANGE: '#f28234',
|
|
34
|
+
SUCCESS: '#10b981',
|
|
35
|
+
WARNING: '#f59e0b',
|
|
36
|
+
ERROR: '#ef4444',
|
|
37
|
+
INFO: '#3b82f6'
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// Message Types
|
|
41
|
+
export const MESSAGE_TYPES = {
|
|
42
|
+
INITIAL_DATA: 'initial_data',
|
|
43
|
+
TEAMS_UPDATE: 'teams_update',
|
|
44
|
+
TASK_UPDATE: 'task_update'
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// Rate Limiting
|
|
48
|
+
export const RATE_LIMIT = {
|
|
49
|
+
WINDOW_MS: 15 * 60 * 1000, // 15 minutes
|
|
50
|
+
MAX_REQUESTS: 100
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// File Watching
|
|
54
|
+
export const WATCH_CONFIG = {
|
|
55
|
+
POLLING_INTERVAL: 1000,
|
|
56
|
+
STABILITY_THRESHOLD: 500,
|
|
57
|
+
POLL_INTERVAL: 100,
|
|
58
|
+
DEPTH: 10
|
|
59
|
+
};
|