1import { useState, useEffect } from 'react';
2import { apiFetch } from '../lib/api';
3import DownloadAgentModal from "../components/DownloadAgentModal";
4import AssignDeviceModal from "../components/AssignDeviceModal";
6import { useNavigate, Link } from 'react-router-dom';
7import MainLayout from '../components/Layout/MainLayout';
8// import removed: TenantSelector
9import '../styles/data-table.css';
10import './Tickets.css';
11import './Monitoring.css';
13function Monitoring() {
14 // All useState hooks should be at the top for proper initialization
15 const navigate = useNavigate();
16 const [agents, setAgents] = useState([]);
17 const [loading, setLoading] = useState(false);
18 const [error, setError] = useState('');
19 const [page, setPage] = useState(1);
20 const [totalPages, setTotalPages] = useState(1);
21 const [search, setSearch] = useState('');
22 const [showFilters, setShowFilters] = useState(false);
23 const tenantsMap = {};
24 const [showDownload, setShowDownload] = useState(false);
25 const [showAssignModal, setShowAssignModal] = useState(false);
26 const [selectedDeviceToAssign, setSelectedDeviceToAssign] = useState(null);
27 const [isMSP, setIsMSP] = useState(false);
28 // Declare token before using it
29 const token = localStorage.getItem('token');
33 const payload = token.split('.')[1];
34 const decoded = JSON.parse(atob(payload.replace(/-/g, '+').replace(/_/g, '/')));
35 tenantId = decoded.tenant_id || decoded.tenantId || '';
39 // Check if user is MSP/admin
43 const payload = token.split('.')[1];
44 const decoded = JSON.parse(atob(payload.replace(/-/g, '+').replace(/_/g, '/')));
45 const userRole = decoded.role || '';
46 const userTenantId = decoded.tenant_id || decoded.tenantId || '';
47 setIsMSP(userRole === 'admin' || userRole === 'msp' || userTenantId === '00000000-0000-0000-0000-000000000001');
51 const [monitoringStats, setMonitoringStats] = useState({
56 // Ensure filters state is defined and used consistently
57 const [filters, setFilters] = useState({
62 // ...existing code...
63 // Reload monitoring data when tenantId changes
65 fetchMonitoringData();
67 // Unified fetch for monitoring stats and agents
68 const fetchMonitoringData = async () => {
71 const params = new URLSearchParams();
72 params.set('page', page);
73 params.set('limit', perPage);
74 if (search) params.set('search', search);
75 // Do not pass tenantId or customerId; backend will determine context from JWT
76 const res = await apiFetch(`/monitoring?${params.toString()}`);
77 if (!res.ok) throw new Error('Failed to load monitoring data');
78 const data = await res.json();
81 online: data.overview?.agentsOnline || 0,
82 total: data.overview?.totalAgents || 0,
83 offline: (data.overview?.totalAgents || 0) - (data.overview?.agentsOnline || 0)
86 setAgents(data.agents || []);
87 setTotalPages(Math.ceil((data.total || (data.agents?.length || 0)) / perPage));
89 console.log('[Monitoring] monitoring data fetched:', data);
91 console.error('[Monitoring] Failed to fetch monitoring data:', err);
92 setMonitoringStats({ online: 0, offline: 0, total: 0 });
94 setError(err.message);
100 const handleAssignDevice = (device) => {
101 setSelectedDeviceToAssign(device);
102 setShowAssignModal(true);
105 const handleDeviceAssigned = (result) => {
106 console.log('Device assigned successfully:', result);
107 // Refresh the monitoring data to show updated assignment
108 fetchMonitoringData();
109 setShowAssignModal(false);
110 setSelectedDeviceToAssign(null);
114 // ...existing code...
116 // ...no localStorage for filters...
118 // Root tenant control logic removed; handled in MainLayout
122 fetchMonitoringData();
123 // Refresh every 30 seconds
124 const interval = setInterval(() => {
125 fetchMonitoringData();
127 // Refresh when page becomes visible (user returns to tab)
128 const handleVisibilityChange = () => {
129 if (!document.hidden) {
130 fetchMonitoringData();
133 document.addEventListener('visibilitychange', handleVisibilityChange);
135 clearInterval(interval);
136 document.removeEventListener('visibilitychange', handleVisibilityChange);
138 }, [page, search, tenantId]);
140 const getStatusClass = (agent) => {
141 // Use meshcentral_connected field from database for accurate real-time status
142 if (agent?.meshcentral_connected === true) return 'status-online';
143 return 'status-offline';
146 const getStatusText = (agent) => {
147 // Use meshcentral_connected field from database for accurate real-time status
148 if (agent?.meshcentral_connected === true) return 'Online';
152 const formatLastSeen = (lastSeen) => {
153 if (!lastSeen) return 'Never';
154 const date = new Date(lastSeen);
155 const now = new Date();
156 const diffMs = now - date;
157 const diffMinutes = Math.floor(diffMs / 1000 / 60);
158 const diffHours = Math.floor(diffMinutes / 60);
159 const diffDays = Math.floor(diffHours / 24);
161 if (diffMinutes < 1) return 'Just now';
162 if (diffMinutes < 60) return `${diffMinutes}m ago`;
163 if (diffHours < 24) return `${diffHours}h ago`;
164 if (diffDays < 7) return `${diffDays}d ago`;
165 return date.toLocaleDateString();
168 const getDeviceIcon = (os) => {
169 if (!os) return 'computer';
170 const osLower = os.toLowerCase();
171 if (osLower.includes('windows')) return 'desktop_windows';
172 if (osLower.includes('linux')) return 'dns';
173 if (osLower.includes('mac')) return 'laptop_mac';
177 const handleFilterChange = (key, value) => {
178 setFilters(prev => ({ ...prev, [key]: value }));
182 const handleClearFilters = () => {
183 const defaultFilters = { type: 'all', status: 'all' };
184 setFilters(defaultFilters);
189 const hasActiveFilters = () => {
190 return filters.type !== 'all' || filters.status !== 'all' || search;
193 // Apply client-side filtering
194 const filteredAgents = agents.filter(agent => {
195 // Exclude default/stale agents
198 agent.agent_uuid === '00000000-0000-0000-0000-000000000001'
201 // Allow untagged devices for MSP users
202 if (agent.is_untagged && isMSP) {
203 return true; // Skip other filters for untagged devices
206 // For non-untagged devices, require enrolled status
207 if (agent.status !== 'enrolled') {
212 if (filters.type !== 'all') {
213 const os = (agent.os || '').toLowerCase();
214 if (filters.type === 'windows' && !os.includes('windows')) return false;
215 if (filters.type === 'linux' && !os.includes('linux')) return false;
216 if (filters.type === 'mac' && !os.includes('mac')) return false;
220 if (filters.status !== 'all') {
221 const statusClass = getStatusClass(agent);
222 if (filters.status === 'online' && statusClass !== 'status-online') return false;
223 if (filters.status === 'offline' && statusClass !== 'status-offline') return false;
229 const onlineCount = agents.filter(a => getStatusClass(a) === 'status-online').length;
231 // Determine if any agent has tenant_id property
232 const showTenantColumn = filteredAgents.some(agent => agent.tenant_id !== undefined && agent.tenant_id !== null);
236 <div className="page-content">
237 <div className="page-header">
238 <h2 style={{margin:0}}>Monitoring</h2>
239 <div className="monitoring-stats">
240 <div className="stat-item">
241 <span className="stat-icon online">
242 <span className="material-symbols-outlined">check_circle</span>
244 <div className="stat-details">
245 <div className="stat-value">{monitoringStats.online}</div>
246 <div className="stat-label">Online</div>
249 <div className="stat-item">
250 <span className="stat-icon offline">
251 <span className="material-symbols-outlined">cancel</span>
253 <div className="stat-details">
254 <div className="stat-value">{monitoringStats.offline}</div>
255 <div className="stat-label">Offline</div>
258 <div className="stat-item">
259 <span className="stat-icon total">
260 <span className="material-symbols-outlined">devices</span>
262 <div className="stat-details">
263 <div className="stat-value">{monitoringStats.total}</div>
264 <div className="stat-label">Total Agents</div>
271 className="btn primary"
272 onClick={() => setShowDownload(true)}
273 title="Deploy Agent or SNMP Device"
274 style={{ display: 'inline-flex', alignItems: 'center', gap: 4 }}
276 <span className="material-symbols-outlined" style={{ fontSize: '18px' }}>
282 <div className="toolbar">
283 <div className="toolbar-left">
284 <div className="search-box">
285 <span className="material-symbols-outlined">search</span>
287 placeholder="Search by name, hostname, customer, or OS..."
289 onChange={e => { setSearch(e.target.value); setPage(1); }}
293 <div className="toolbar-right">
294 <button className="btn" onClick={fetchMonitoringData}>
295 <span className="material-symbols-outlined">refresh</span>
299 className={`btn ${showFilters ? 'active' : ''}`}
300 onClick={() => setShowFilters(!showFilters)}
301 title="Toggle filters"
303 <span className="material-symbols-outlined">tune</span>
305 {hasActiveFilters() && (
306 <span className="badge">{Object.values(filters).filter(v => v && v !== 'all').length + (search ? 1 : 0)}</span>
309 {hasActiveFilters() && (
310 <button className="btn" onClick={handleClearFilters}>
311 <span className="material-symbols-outlined">clear_all</span>
319 <div className="filter-panel">
320 <h3>Filter Options</h3>
321 <div className="filter-grid">
322 <div className="filter-group">
323 <label>OS Type</label>
326 onChange={e => handleFilterChange('type', e.target.value)}
328 <option value="all">All Types</option>
329 <option value="windows">Windows</option>
330 <option value="linux">Linux</option>
331 <option value="mac">macOS</option>
335 <div className="filter-group">
336 <label>Status</label>
338 value={filters.status}
339 onChange={e => handleFilterChange('status', e.target.value)}
341 <option value="all">All Statuses</option>
342 <option value="online">Online</option>
343 <option value="offline">Offline</option>
348 {hasActiveFilters() && (
349 <div className="active-filters">
350 <div className="active-filters-header">
351 <span className="filter-label">Active Filters:</span>
353 <div className="filter-tags">
354 {filters.type !== 'all' && (
355 <span className="filter-tag">
357 <button onClick={(e) => { e.stopPropagation(); handleFilterChange('type', 'all'); }}>×</button>
360 {filters.status !== 'all' && (
361 <span className="filter-tag">
362 Status: {filters.status}
363 <button onClick={(e) => { e.stopPropagation(); handleFilterChange('status', 'all'); }}>×</button>
367 <span className="filter-tag">
369 <button onClick={(e) => { e.stopPropagation(); setSearch(''); }}>×</button>
378 {error && <div className="error-message">{error}</div>}
381 <div className="loading">Loading agents...</div>
384 <div className="table-container">
385 <table className="data-table">
390 {showTenantColumn && <th>Tenant</th>}
396 {isMSP && <th>Actions</th>}
400 {filteredAgents.length === 0 ? (
402 <td colSpan={isMSP ? (showTenantColumn ? "9" : "8") : (showTenantColumn ? "8" : "7")} style={{ textAlign: 'center', padding: '2rem', color: 'var(--text-secondary)' }}>
407 filteredAgents.map((agent) => (
408 <tr key={agent.agent_uuid || agent.id}>
411 className={`material-symbols-outlined device-icon ${getStatusClass(agent)}`}
412 title={getStatusClass(agent) === 'status-online' ? 'Online (connected)' : 'Offline'}
413 style={{ color: getStatusClass(agent) === 'status-online' ? '#2ecc40' : undefined }}
415 {getDeviceIcon(agent.os)}
419 <div className="agent-name-cell">
420 <div className="agent-name">
421 <Link to={`/agents/${agent.agent_uuid}`}>
422 {agent.name || agent.hostname || 'Unnamed'}
425 <div className="agent-hostname">{agent.hostname}</div>
429 {showTenantColumn && (
430 <td title={agent.tenant_id ? `Tenant #${agent.tenant_id}` : 'Not assigned'}>
431 {agent.is_untagged ? (
432 <span className="status-badge inactive" style={{ background: '#ff9800', color: 'white' }}>
435 ) : agent.tenant_id === 1 ? (
438 agent.tenant_name || tenantsMap?.[agent.tenant_id] || (agent.tenant_id ? `#${agent.tenant_id}` : '—')
444 <span className="customer-name">
445 {agent.customer_name || (agent.customer_id ? `#${agent.customer_id}` : 'No Customer')}
449 <td><span className="os-badge">{agent.os || '—'}</span></td>
451 <td><span className="os-version">{agent.os_version || '—'}</span></td>
454 {agent.contract_title ? (
455 <span className="status-badge active">{agent.contract_title}</span>
457 <span className="status-badge inactive">No Contract</span>
462 <div className="last-seen-cell">
463 {agent.meshcentral_connected ? (
464 <span className="status-text status-online">
468 <span className="time-ago">
469 {formatLastSeen(agent.last_seen)}
477 {agent.is_untagged ? (
479 className="btn btn-sm primary"
480 onClick={() => handleAssignDevice(agent)}
481 title="Assign this device to a tenant"
482 style={{ fontSize: '0.875rem', padding: '0.25rem 0.75rem' }}
484 <span className="material-symbols-outlined" style={{ fontSize: '16px', marginRight: '4px' }}>
490 <span style={{ color: 'var(--text-secondary)', fontSize: '0.875rem' }}>—</span>
500 <div className="pagination">
501 <button className="btn" disabled={page === 1} onClick={() => setPage(p => Math.max(1, p - 1))}>
502 <span className="material-symbols-outlined">navigate_before</span>
505 <span>Page {page} of {totalPages}</span>
507 <button className="btn" disabled={page === totalPages} onClick={() => setPage(p => Math.min(totalPages, p + 1))}>
508 <span className="material-symbols-outlined">navigate_next</span>
515 isOpen={showDownload}
516 onClose={() => setShowDownload(false)}
521 {showAssignModal && selectedDeviceToAssign && (
523 isOpen={showAssignModal}
525 setShowAssignModal(false);
526 setSelectedDeviceToAssign(null);
528 device={selectedDeviceToAssign}
529 onAssigned={handleDeviceAssigned}
537export default Monitoring;