EverydayTech Platform - Developer Reference
Complete Source Code Documentation - All Applications
Loading...
Searching...
No Matches
TabHosting.jsx
Go to the documentation of this file.
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';
6
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({
16 customerId: '',
17 notes: ''
18 });
19 const { toasts, showToast, hideToast } = useToast();
20
21 useEffect(() => {
22 fetchWordPressSites();
23 fetchCustomers();
24
25 // Auto-refresh every 5 minutes
26 const interval = setInterval(fetchWordPressSites, 5 * 60 * 1000);
27 return () => clearInterval(interval);
28 }, []);
29
30 const fetchWordPressSites = async () => {
31 setLoading(true);
32 setError('');
33 try {
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 => ({
41 ...site,
42 name: site.domain,
43 server_ip: latestReport.server_ip,
44 server_hostname: latestReport.server_hostname,
45 last_checked: latestReport.timestamp,
46 last_scan: latestReport.last_scan
47 }));
48 setWordPressSites(sites);
49 setLastUpdate(latestReport.received_at || latestReport.timestamp);
50 }
51 } else {
52 setWordPressSites([]);
53 }
54 } else {
55 setError('Failed to fetch WordPress sites');
56 }
57 } catch (err) {
58 console.error('Error fetching WordPress sites:', err);
59 setError(err.message || 'Failed to load WordPress sites');
60 } finally {
61 setLoading(false);
62 }
63 };
64
65 const fetchCustomers = async () => {
66 try {
67 const res = await apiFetch('/customers');
68 if (res.ok) {
69 const data = await res.json();
70 setCustomers(data.customers || []);
71 }
72 } catch (err) {
73 console.error('Error fetching customers:', err);
74 }
75 };
76
77 const handleAssignSite = (site) => {
78 setEditingSite(site);
79 setAssignmentData({
80 customerId: site.customer_id || '',
81 notes: site.notes || ''
82 });
83 };
84
85 const handleSaveAssignment = async () => {
86 if (!editingSite) return;
87
88 try {
89 const res = await apiFetch(`/wordpress/sites/${editingSite.domain}/assign`, {
90 method: 'POST',
91 headers: { 'Content-Type': 'application/json' },
92 body: JSON.stringify(assignmentData)
93 });
94
95 if (res.ok) {
96 showToast('Site assignment updated successfully', 'success', 3000);
97 setEditingSite(null);
98 fetchWordPressSites();
99 } else {
100 const errorData = await res.json();
101 showToast(errorData.error || 'Failed to update assignment', 'error', 5000);
102 }
103 } catch (err) {
104 console.error('Error saving assignment:', err);
105 showToast(err.message || 'Failed to save assignment', 'error', 5000);
106 }
107 };
108
109 const getStatusColor = (status) => {
110 const statusMap = {
111 'online': 'success',
112 'error': 'danger',
113 'timeout': 'warning',
114 'offline': 'danger',
115 'unknown': 'secondary'
116 };
117 return statusMap[status?.toLowerCase()] || 'secondary';
118 };
119
120 const handleRefresh = () => {
121 fetchWordPressSites();
122 showToast('Refreshing WordPress sites...', 'info', 2000);
123 };
124
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);
130
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`;
135 };
136
137 const getCustomerName = (customerId) => {
138 const customer = customers.find(c => c.customer_id === customerId);
139 return customer ? customer.company_name || customer.name : '';
140 };
141
142 if (loading && wordpressSites.length === 0) {
143 return (
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>
147 </div>
148 );
149 }
150
151 return (
152 <>
153 <Toast toasts={toasts} hideToast={hideToast} />
154
155 <div style={{ padding: '20px' }}>
156 <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '20px' }}>
157 <div>
158 <h2>LAMP WordPress Management</h2>
159 <p style={{ color: 'var(--text-secondary)', marginTop: '8px' }}>
160 Manage self-hosted WordPress sites on LAMP servers
161 </p>
162 </div>
163 <div style={{ display: 'flex', gap: '12px' }}>
164 <button
165 onClick={handleRefresh}
166 disabled={loading}
167 className="btn btn-primary"
168 style={{ display: 'flex', alignItems: 'center', gap: '8px' }}
169 >
170 <span className="material-symbols-outlined" style={{ animation: loading ? 'spin 1s linear infinite' : 'none' }}>
171 refresh
172 </span>
173 {loading ? 'Refreshing...' : 'Refresh'}
174 </button>
175 </div>
176 </div>
177
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' }}>
182 dns
183 </span>
184 <div>
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'}
189 </div>
190 {lastUpdate && (
191 <div style={{ fontSize: '12px', color: 'var(--info-text)', opacity: 0.8, marginTop: '4px' }}>
192 Last updated {formatTime(lastUpdate)} • {wordpressSites.length} sites monitored
193 </div>
194 )}
195 </div>
196 </div>
197 </div>
198
199 {error && (
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)' }}>
203 error
204 </span>
205 <div style={{ fontSize: '14px', color: 'var(--error-text)' }}>
206 {error}
207 </div>
208 </div>
209 </div>
210 )}
211
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 }}>
216 web_asset_off
217 </span>
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
221 </p>
222 </div>
223 ) : (
224 <div style={{ display: 'grid', gap: '24px', gridTemplateColumns: 'repeat(auto-fill, minmax(380px, 1fr))' }}>
225 {wordpressSites.map((site, index) => (
226 <div
227 key={site.domain || index}
228 className="card"
229 style={{
230 overflow: 'hidden',
231 transition: 'transform 0.2s, box-shadow 0.2s',
232 border: site.malware_issues > 0 ? '1px solid var(--danger-border)' : '1px solid var(--border)'
233 }}
234 onMouseEnter={(e) => {
235 e.currentTarget.style.transform = 'translateY(-2px)';
236 e.currentTarget.style.boxShadow = '0 4px 12px rgba(0,0,0,0.15)';
237 }}
238 onMouseLeave={(e) => {
239 e.currentTarget.style.transform = 'translateY(0)';
240 e.currentTarget.style.boxShadow = '';
241 }}
242 >
243 <div style={{
244 padding: '20px',
245 borderBottom: '1px solid var(--border)',
246 background: site.malware_issues > 0 ? 'var(--danger-bg)' : 'transparent'
247 }}>
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={{
252 fontSize: '24px',
253 color: 'var(--primary)',
254 opacity: 0.9
255 }}>
256 web
257 </span>
258 <h3 style={{
259 margin: 0,
260 fontSize: '17px',
261 fontWeight: '600',
262 color: 'var(--text-primary)'
263 }}>{site.domain}</h3>
264 </div>
265 {site.customer_id && (
266 <p style={{ fontSize: '13px', color: 'var(--text-secondary)', margin: '4px 0 0 34px' }}>
267 {getCustomerName(site.customer_id)}
268 </p>
269 )}
270 </div>
271 <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-end', gap: '6px' }}>
272 <span className={`badge badge-${getStatusColor(site.status)}`}>
273 {site.status || 'unknown'}
274 </span>
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' : ''}
279 </span>
280 )}
281 </div>
282 </div>
283
284 <a
285 href={`https://${site.domain}`}
286 target="_blank"
287 rel="noopener noreferrer"
288 style={{
289 fontSize: '13px',
290 color: 'var(--primary)',
291 textDecoration: 'none',
292 display: 'inline-flex',
293 alignItems: 'center',
294 gap: '4px',
295 padding: '4px 0',
296 transition: 'opacity 0.2s'
297 }}
298 onMouseEnter={(e) => e.currentTarget.style.opacity = '0.7'}
299 onMouseLeave={(e) => e.currentTarget.style.opacity = '1'}
300 >
301 <span className="material-symbols-outlined" style={{ fontSize: '14px' }}>
302 open_in_new
303 </span>
304 <span>Visit Site</span>
305 </a>
306 </div>
307
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>
313 </div>
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>
317 </div>
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>
321 </div>
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>
325 </div>
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)}
331 </span>
332 </div>
333 )}
334 </div>
335
336 <button
337 className="btn btn-primary"
338 onClick={() => navigate(`/wordpress/${encodeURIComponent(site.domain)}`)}
339 style={{
340 width: '100%',
341 display: 'flex',
342 alignItems: 'center',
343 justifyContent: 'center',
344 gap: '8px',
345 padding: '12px'
346 }}
347 >
348 <span className="material-symbols-outlined" style={{ fontSize: '18px' }}>
349 settings
350 </span>
351 Manage Site
352 </button>
353 </div>
354 </div>
355 ))}
356 </div>
357 )}
358 </div>
359
360 {/* Assignment Modal */}
361 {editingSite && (
362 <div style={{
363 position: 'fixed',
364 top: 0,
365 left: 0,
366 right: 0,
367 bottom: 0,
368 background: 'rgba(0,0,0,0.5)',
369 display: 'flex',
370 alignItems: 'center',
371 justifyContent: 'center',
372 zIndex: 1000
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)' }}>
378 {editingSite.domain}
379 </p>
380 </div>
381 <div style={{ padding: '20px' }}>
382 <div style={{ marginBottom: '16px' }}>
383 <label style={{ display: 'block', marginBottom: '8px', fontSize: '14px', fontWeight: '500' }}>
384 Customer
385 </label>
386 <select
387 value={assignmentData.customerId}
388 onChange={e => setAssignmentData({ ...assignmentData, customerId: e.target.value })}
389 style={{
390 width: '100%',
391 padding: '8px 12px',
392 border: '1px solid var(--border)',
393 borderRadius: '4px',
394 fontSize: '14px'
395 }}
396 >
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}
401 </option>
402 ))}
403 </select>
404 </div>
405 <div style={{ marginBottom: '16px' }}>
406 <label style={{ display: 'block', marginBottom: '8px', fontSize: '14px', fontWeight: '500' }}>
407 Notes (optional)
408 </label>
409 <textarea
410 value={assignmentData.notes}
411 onChange={e => setAssignmentData({ ...assignmentData, notes: e.target.value })}
412 placeholder="Add any notes about this site..."
413 rows={3}
414 style={{
415 width: '100%',
416 padding: '8px 12px',
417 border: '1px solid var(--border)',
418 borderRadius: '4px',
419 fontSize: '14px',
420 resize: 'vertical'
421 }}
422 />
423 </div>
424 </div>
425 <div style={{ padding: '16px 20px', borderTop: '1px solid var(--border)', display: 'flex', gap: '12px', justifyContent: 'flex-end' }}>
426 <button
427 onClick={() => setEditingSite(null)}
428 className="btn"
429 >
430 Cancel
431 </button>
432 <button
433 onClick={handleSaveAssignment}
434 className="btn btn-primary"
435 >
436 Save Assignment
437 </button>
438 </div>
439 </div>
440 </div>
441 )}
442 </>
443 );
444}