1import { useState, useEffect } from 'react';
2import { useNavigate } from 'react-router-dom';
3import { apiFetch } from '../../lib/api';
4import { useToast } from '../../hooks/useToast';
5import Toast from '../../components/Toast';
7export default function TabHosting() {
8 const navigate = useNavigate();
9 const [wordpressSites, setWordPressSites] = useState([]);
10 const [loading, setLoading] = useState(true);
11 const [error, setError] = useState('');
12 const [customers, setCustomers] = useState([]);
13 const [lastUpdate, setLastUpdate] = useState(null);
14 const [editingSite, setEditingSite] = useState(null);
15 const [assignmentData, setAssignmentData] = useState({
19 const { toasts, showToast, hideToast } = useToast();
22 fetchWordPressSites();
25 // Auto-refresh every 5 minutes
26 const interval = setInterval(fetchWordPressSites, 5 * 60 * 1000);
27 return () => clearInterval(interval);
30 const fetchWordPressSites = async () => {
34 const monitoringRes = await apiFetch('/wordpress/monitoring/latest');
35 if (monitoringRes.ok) {
36 const monitoringData = await monitoringRes.json();
37 if (monitoringData.reports && monitoringData.reports.length > 0) {
38 const latestReport = monitoringData.reports[0];
39 if (latestReport && latestReport.sites) {
40 const sites = latestReport.sites.map(site => ({
43 server_ip: latestReport.server_ip,
44 server_hostname: latestReport.server_hostname,
45 last_checked: latestReport.timestamp,
46 last_scan: latestReport.last_scan
48 setWordPressSites(sites);
49 setLastUpdate(latestReport.received_at || latestReport.timestamp);
52 setWordPressSites([]);
55 setError('Failed to fetch WordPress sites');
58 console.error('Error fetching WordPress sites:', err);
59 setError(err.message || 'Failed to load WordPress sites');
65 const fetchCustomers = async () => {
67 const res = await apiFetch('/customers');
69 const data = await res.json();
70 setCustomers(data.customers || []);
73 console.error('Error fetching customers:', err);
77 const handleAssignSite = (site) => {
80 customerId: site.customer_id || '',
81 notes: site.notes || ''
85 const handleSaveAssignment = async () => {
86 if (!editingSite) return;
89 const res = await apiFetch(`/wordpress/sites/${editingSite.domain}/assign`, {
91 headers: { 'Content-Type': 'application/json' },
92 body: JSON.stringify(assignmentData)
96 showToast('Site assignment updated successfully', 'success', 3000);
98 fetchWordPressSites();
100 const errorData = await res.json();
101 showToast(errorData.error || 'Failed to update assignment', 'error', 5000);
104 console.error('Error saving assignment:', err);
105 showToast(err.message || 'Failed to save assignment', 'error', 5000);
109 const getStatusColor = (status) => {
113 'timeout': 'warning',
115 'unknown': 'secondary'
117 return statusMap[status?.toLowerCase()] || 'secondary';
120 const handleRefresh = () => {
121 fetchWordPressSites();
122 showToast('Refreshing WordPress sites...', 'info', 2000);
125 const formatTime = (date) => {
126 if (!date) return '';
127 const now = new Date();
128 const then = new Date(date);
129 const diff = Math.floor((now - then) / 1000);
131 if (diff < 60) return `${diff} seconds ago`;
132 if (diff < 3600) return `${Math.floor(diff / 60)} minutes ago`;
133 if (diff < 86400) return `${Math.floor(diff / 3600)} hours ago`;
134 return `${Math.floor(diff / 86400)} days ago`;
137 const getCustomerName = (customerId) => {
138 const customer = customers.find(c => c.customer_id === customerId);
139 return customer ? customer.company_name || customer.name : '';
142 if (loading && wordpressSites.length === 0) {
144 <div style={{ padding: '40px', textAlign: 'center' }}>
145 <div className="spinner"></div>
146 <p style={{ marginTop: '16px', color: 'var(--text-secondary)' }}>Loading LAMP WordPress sites...</p>
153 <Toast toasts={toasts} hideToast={hideToast} />
155 <div style={{ padding: '20px' }}>
156 <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '20px' }}>
158 <h2>LAMP WordPress Management</h2>
159 <p style={{ color: 'var(--text-secondary)', marginTop: '8px' }}>
160 Manage self-hosted WordPress sites on LAMP servers
163 <div style={{ display: 'flex', gap: '12px' }}>
165 onClick={handleRefresh}
167 className="btn btn-primary"
168 style={{ display: 'flex', alignItems: 'center', gap: '8px' }}
170 <span className="material-symbols-outlined" style={{ animation: loading ? 'spin 1s linear infinite' : 'none' }}>
173 {loading ? 'Refreshing...' : 'Refresh'}
178 {/* Server Status Card */}
179 <div className="card" style={{ padding: '16px', marginBottom: '20px', background: 'var(--info-bg)', border: '1px solid var(--info-border)' }}>
180 <div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
181 <span className="material-symbols-outlined" style={{ color: 'var(--info-text)', fontSize: '24px' }}>
185 <div style={{ fontSize: '14px', color: 'var(--info-text)', fontWeight: '500' }}>
186 {wordpressSites.length > 0 && wordpressSites[0].server_ip ?
187 `LAMP Server: ${wordpressSites[0].server_hostname || wordpressSites[0].server_ip}` :
188 'No monitoring data available'}
191 <div style={{ fontSize: '12px', color: 'var(--info-text)', opacity: 0.8, marginTop: '4px' }}>
192 Last updated {formatTime(lastUpdate)} • {wordpressSites.length} sites monitored
200 <div className="card" style={{ padding: '16px', marginBottom: '20px', background: 'var(--error-bg)', border: '1px solid var(--error-border)' }}>
201 <div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
202 <span className="material-symbols-outlined" style={{ color: 'var(--error-text)' }}>
205 <div style={{ fontSize: '14px', color: 'var(--error-text)' }}>
212 {/* WordPress Sites Grid */}
213 {wordpressSites.length === 0 ? (
214 <div className="card" style={{ padding: '40px', textAlign: 'center' }}>
215 <span className="material-symbols-outlined" style={{ fontSize: '48px', color: 'var(--text-secondary)', opacity: 0.5 }}>
218 <h3 style={{ marginTop: '16px', color: 'var(--text-secondary)' }}>No WordPress Sites Found</h3>
219 <p style={{ color: 'var(--text-secondary)', marginTop: '8px' }}>
220 Waiting for monitoring data from LAMP servers
224 <div style={{ display: 'grid', gap: '24px', gridTemplateColumns: 'repeat(auto-fill, minmax(380px, 1fr))' }}>
225 {wordpressSites.map((site, index) => (
227 key={site.domain || index}
231 transition: 'transform 0.2s, box-shadow 0.2s',
232 border: site.malware_issues > 0 ? '1px solid var(--danger-border)' : '1px solid var(--border)'
234 onMouseEnter={(e) => {
235 e.currentTarget.style.transform = 'translateY(-2px)';
236 e.currentTarget.style.boxShadow = '0 4px 12px rgba(0,0,0,0.15)';
238 onMouseLeave={(e) => {
239 e.currentTarget.style.transform = 'translateY(0)';
240 e.currentTarget.style.boxShadow = '';
245 borderBottom: '1px solid var(--border)',
246 background: site.malware_issues > 0 ? 'var(--danger-bg)' : 'transparent'
248 <div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', marginBottom: '12px' }}>
249 <div style={{ flex: 1 }}>
250 <div style={{ display: 'flex', alignItems: 'center', gap: '10px', marginBottom: '6px' }}>
251 <span className="material-symbols-outlined" style={{
253 color: 'var(--primary)',
262 color: 'var(--text-primary)'
263 }}>{site.domain}</h3>
265 {site.customer_id && (
266 <p style={{ fontSize: '13px', color: 'var(--text-secondary)', margin: '4px 0 0 34px' }}>
267 {getCustomerName(site.customer_id)}
271 <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-end', gap: '6px' }}>
272 <span className={`badge badge-${getStatusColor(site.status)}`}>
273 {site.status || 'unknown'}
275 {site.malware_issues > 0 && (
276 <span className="badge badge-danger" style={{ fontSize: '11px' }}>
277 <span className="material-symbols-outlined" style={{ fontSize: '12px', marginRight: '2px' }}>warning</span>
278 {site.malware_issues} issue{site.malware_issues !== 1 ? 's' : ''}
285 href={`https://${site.domain}`}
287 rel="noopener noreferrer"
290 color: 'var(--primary)',
291 textDecoration: 'none',
292 display: 'inline-flex',
293 alignItems: 'center',
296 transition: 'opacity 0.2s'
298 onMouseEnter={(e) => e.currentTarget.style.opacity = '0.7'}
299 onMouseLeave={(e) => e.currentTarget.style.opacity = '1'}
301 <span className="material-symbols-outlined" style={{ fontSize: '14px' }}>
304 <span>Visit Site</span>
308 <div style={{ padding: '20px' }}>
309 <div style={{ display: 'grid', gap: '10px', fontSize: '13px', marginBottom: '20px' }}>
310 <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
311 <span style={{ color: 'var(--text-secondary)', fontWeight: '500' }}>WordPress</span>
312 <span style={{ fontFamily: 'monospace', fontSize: '12px' }}>{site.wp_version?.trim().replace(/.*\$wp_version\s+/, '') || 'Unknown'}</span>
314 <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
315 <span style={{ color: 'var(--text-secondary)', fontWeight: '500' }}>Plugins</span>
316 <span>{site.plugin_count || 0}</span>
318 <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
319 <span style={{ color: 'var(--text-secondary)', fontWeight: '500' }}>Disk Usage</span>
320 <span style={{ fontFamily: 'monospace', fontSize: '12px' }}>{site.disk_usage || 'N/A'}</span>
322 <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
323 <span style={{ color: 'var(--text-secondary)', fontWeight: '500' }}>Database</span>
324 <span style={{ fontFamily: 'monospace', fontSize: '11px', color: 'var(--text-secondary)' }}>{site.database || 'N/A'}</span>
326 {site.last_checked && (
327 <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', paddingTop: '6px', borderTop: '1px solid var(--border)' }}>
328 <span style={{ color: 'var(--text-secondary)', fontWeight: '500', fontSize: '12px' }}>Last Checked</span>
329 <span style={{ fontSize: '12px', color: 'var(--text-secondary)' }}>
330 {formatTime(site.last_checked)}
337 className="btn btn-primary"
338 onClick={() => navigate(`/wordpress/${encodeURIComponent(site.domain)}`)}
342 alignItems: 'center',
343 justifyContent: 'center',
348 <span className="material-symbols-outlined" style={{ fontSize: '18px' }}>
360 {/* Assignment Modal */}
368 background: 'rgba(0,0,0,0.5)',
370 alignItems: 'center',
371 justifyContent: 'center',
373 }} onClick={() => setEditingSite(null)}>
374 <div className="card" style={{ width: '500px', maxWidth: '90vw', padding: 0 }} onClick={e => e.stopPropagation()}>
375 <div style={{ padding: '20px', borderBottom: '1px solid var(--border)' }}>
376 <h3 style={{ margin: 0 }}>Assign Site to Customer</h3>
377 <p style={{ margin: '4px 0 0 0', fontSize: '14px', color: 'var(--text-secondary)' }}>
381 <div style={{ padding: '20px' }}>
382 <div style={{ marginBottom: '16px' }}>
383 <label style={{ display: 'block', marginBottom: '8px', fontSize: '14px', fontWeight: '500' }}>
387 value={assignmentData.customerId}
388 onChange={e => setAssignmentData({ ...assignmentData, customerId: e.target.value })}
392 border: '1px solid var(--border)',
397 <option value="">-- Select Customer --</option>
398 {customers.map(customer => (
399 <option key={customer.customer_id} value={customer.customer_id}>
400 {customer.company_name || customer.name}
405 <div style={{ marginBottom: '16px' }}>
406 <label style={{ display: 'block', marginBottom: '8px', fontSize: '14px', fontWeight: '500' }}>
410 value={assignmentData.notes}
411 onChange={e => setAssignmentData({ ...assignmentData, notes: e.target.value })}
412 placeholder="Add any notes about this site..."
417 border: '1px solid var(--border)',
425 <div style={{ padding: '16px 20px', borderTop: '1px solid var(--border)', display: 'flex', gap: '12px', justifyContent: 'flex-end' }}>
427 onClick={() => setEditingSite(null)}
433 onClick={handleSaveAssignment}
434 className="btn btn-primary"