EverydayTech Platform - Developer Reference
Complete Source Code Documentation - All Applications
Loading...
Searching...
No Matches
WordPressSite.jsx
Go to the documentation of this file.
1// =============================================
2// WordPressSite.jsx — WordPress Site Detail Page
3// =============================================
4import React, { useState, useEffect } from "react";
5import { useParams, useNavigate } from "react-router-dom";
6import { apiFetch } from "../lib/api";
7import MainLayout from "../components/Layout/MainLayout";
8import Toast from "../components/Toast";
9import "./wordpress-site.css";
10
11export default function WordPressSite() {
12 const { domain } = useParams();
13 const navigate = useNavigate();
14 const [site, setSite] = useState(null);
15 const [siteDetails, setSiteDetails] = useState(null);
16 const [loading, setLoading] = useState(true);
17 const [detailsLoading, setDetailsLoading] = useState(true);
18 const [updating, setUpdating] = useState({});
19 const [toasts, setToasts] = useState([]);
20 const [loginCredentials, setLoginCredentials] = useState(null);
21 const [users, setUsers] = useState([]);
22 const [usersLoading, setUsersLoading] = useState(true);
23 const [bulkUpdateProgress, setBulkUpdateProgress] = useState({ current: 0, total: 0, type: '' });
24
25 useEffect(() => {
26 if (!domain) return;
27 fetchSiteDetails();
28 fetchDetailedInfo();
29 fetchUsers();
30 // Refresh data every 30 seconds
31 const interval = setInterval(() => {
32 fetchSiteDetails();
33 fetchDetailedInfo();
34 fetchUsers();
35 }, 30000);
36 return () => clearInterval(interval);
37 }, [domain]);
38
39 const fetchSiteDetails = async () => {
40 try {
41 // First get the site from monitoring data
42 const monitoringRes = await apiFetch('/wordpress/monitoring/latest');
43 if (monitoringRes.ok) {
44 const data = await monitoringRes.json();
45 if (data.reports && data.reports.length > 0) {
46 const latestReport = data.reports[0];
47 const siteData = latestReport.sites.find(s => s.domain === domain);
48
49 if (siteData) {
50 setSite({
51 ...siteData,
52 server_ip: latestReport.server_ip,
53 server_hostname: latestReport.server_hostname,
54 last_checked: latestReport.timestamp,
55 last_scan: latestReport.last_scan
56 });
57 }
58 }
59 }
60 } catch (err) {
61 console.error('Error fetching site details:', err);
62 showToast('Failed to load site details', 'error');
63 } finally {
64 setLoading(false);
65 }
66 };
67
68 const fetchDetailedInfo = async () => {
69 try {
70 const res = await apiFetch(`/wordpress/sites/${domain}/details`);
71 if (res.ok) {
72 const data = await res.json();
73 setSiteDetails(data);
74 }
75 } catch (err) {
76 console.error('Error fetching detailed site info:', err);
77 } finally {
78 setDetailsLoading(false);
79 }
80 };
81
82 const fetchUsers = async () => {
83 try {
84 const res = await apiFetch(`/wordpress/sites/${domain}/users`);
85 if (res.ok) {
86 const data = await res.json();
87 setUsers(data.users || []);
88 }
89 } catch (err) {
90 console.error('Error fetching users:', err);
91 } finally {
92 setUsersLoading(false);
93 }
94 };
95
96 const showToast = (message, type = 'info', duration = 3000) => {
97 const id = Date.now();
98 setToasts(prev => [...prev, { id, message, type }]);
99 setTimeout(() => hideToast(id), duration);
100 };
101
102 const hideToast = (id) => {
103 setToasts(prev => prev.filter(t => t.id !== id));
104 };
105
106 const copyToClipboard = (text, label) => {
107 navigator.clipboard.writeText(text).then(() => {
108 showToast(`${label} copied to clipboard`, 'success', 2000);
109 }).catch(() => {
110 showToast('Failed to copy', 'error');
111 });
112 };
113
114 const handlePluginUpdate = async (plugin) => {
115 setUpdating(prev => ({ ...prev, [`plugin-${plugin}`]: true }));
116 try {
117 const res = await apiFetch(`/wordpress/sites/${domain}/plugin/${plugin}/update`, {
118 method: 'POST'
119 });
120 if (res.ok) {
121 showToast(`Plugin ${plugin} updated successfully`, 'success');
122 fetchSiteDetails();
123 fetchDetailedInfo();
124 } else {
125 const err = await res.json();
126 showToast(err.error || 'Update failed', 'error');
127 }
128 } catch (err) {
129 showToast('Update failed', 'error');
130 } finally {
131 setUpdating(prev => ({ ...prev, [`plugin-${plugin}`]: false }));
132 }
133 };
134
135 const handleThemeUpdate = async (theme) => {
136 setUpdating(prev => ({ ...prev, [`theme-${theme}`]: true }));
137 try {
138 const res = await apiFetch(`/wordpress/sites/${domain}/theme/${theme}/update`, {
139 method: 'POST'
140 });
141 if (res.ok) {
142 showToast(`Theme ${theme} updated successfully`, 'success');
143 fetchSiteDetails();
144 fetchDetailedInfo();
145 } else {
146 const err = await res.json();
147 showToast(err.error || 'Update failed', 'error');
148 }
149 } catch (err) {
150 showToast('Update failed', 'error');
151 } finally {
152 setUpdating(prev => ({ ...prev, [`theme-${theme}`]: false }));
153 }
154 };
155
156 const handleCoreUpdate = async () => {
157 setUpdating(prev => ({ ...prev, 'core': true }));
158 try {
159 const res = await apiFetch(`/wordpress/sites/${domain}/core/update`, {
160 method: 'POST'
161 });
162 if (res.ok) {
163 showToast('WordPress core updated successfully', 'success');
164 fetchSiteDetails();
165 fetchDetailedInfo();
166 } else {
167 const err = await res.json();
168 showToast(err.error || 'Update failed', 'error');
169 }
170 } catch (err) {
171 showToast('Update failed', 'error');
172 } finally {
173 setUpdating(prev => ({ ...prev, 'core': false }));
174 }
175 };
176
177 const handleCleanMalware = async () => {
178 if (!confirm('This will remove detected malicious files. Continue?')) return;
179
180 setUpdating(prev => ({ ...prev, 'clean': true }));
181 try {
182 const res = await apiFetch(`/wordpress/sites/${domain}/clean`, {
183 method: 'POST'
184 });
185 if (res.ok) {
186 const result = await res.json();
187 showToast(`Cleanup complete: ${result.files_removed || 0} files removed`, 'success');
188 fetchSiteDetails();
189 fetchDetailedInfo();
190 } else {
191 const err = await res.json();
192 showToast(err.error || 'Cleanup failed', 'error');
193 }
194 } catch (err) {
195 showToast('Cleanup failed', 'error');
196 } finally {
197 setUpdating(prev => ({ ...prev, 'clean': false }));
198 }
199 };
200
201 const handleUpdateAllPlugins = async () => {
202 const pluginsToUpdate = siteDetails?.plugins?.filter(p => p.update_available === 'available') || [];
203
204 if (pluginsToUpdate.length === 0) {
205 showToast('No plugin updates available', 'info');
206 return;
207 }
208
209 if (!confirm(`Update ${pluginsToUpdate.length} plugin(s)? This may take several minutes.`)) return;
210
211 setBulkUpdateProgress({ current: 0, total: pluginsToUpdate.length, type: 'plugins' });
212 setUpdating(prev => ({ ...prev, 'bulk-plugins': true }));
213
214 let successCount = 0;
215 let failCount = 0;
216
217 for (let i = 0; i < pluginsToUpdate.length; i++) {
218 const plugin = pluginsToUpdate[i];
219 setBulkUpdateProgress({ current: i + 1, total: pluginsToUpdate.length, type: 'plugins' });
220
221 try {
222 const res = await apiFetch(`/wordpress/sites/${domain}/plugin/${plugin.name}/update`, {
223 method: 'POST'
224 });
225 if (res.ok) {
226 successCount++;
227 showToast(`✓ Updated ${plugin.title || plugin.name}`, 'success', 2000);
228 } else {
229 failCount++;
230 showToast(`✗ Failed to update ${plugin.title || plugin.name}`, 'error', 2000);
231 }
232 } catch (err) {
233 failCount++;
234 showToast(`✗ Failed to update ${plugin.title || plugin.name}`, 'error', 2000);
235 }
236
237 // Small delay to avoid overwhelming the server
238 if (i < pluginsToUpdate.length - 1) {
239 await new Promise(resolve => setTimeout(resolve, 500));
240 }
241 }
242
243 setBulkUpdateProgress({ current: 0, total: 0, type: '' });
244 setUpdating(prev => ({ ...prev, 'bulk-plugins': false }));
245
246 showToast(`Bulk update complete: ${successCount} succeeded, ${failCount} failed`,
247 failCount === 0 ? 'success' : 'warning', 5000);
248
249 fetchSiteDetails();
250 fetchDetailedInfo();
251 };
252
253 const handleUpdateAllThemes = async () => {
254 const themesToUpdate = siteDetails?.themes?.filter(t => t.update_available === 'available') || [];
255
256 if (themesToUpdate.length === 0) {
257 showToast('No theme updates available', 'info');
258 return;
259 }
260
261 if (!confirm(`Update ${themesToUpdate.length} theme(s)? This may take several minutes.`)) return;
262
263 setBulkUpdateProgress({ current: 0, total: themesToUpdate.length, type: 'themes' });
264 setUpdating(prev => ({ ...prev, 'bulk-themes': true }));
265
266 let successCount = 0;
267 let failCount = 0;
268
269 for (let i = 0; i < themesToUpdate.length; i++) {
270 const theme = themesToUpdate[i];
271 setBulkUpdateProgress({ current: i + 1, total: themesToUpdate.length, type: 'themes' });
272
273 try {
274 const res = await apiFetch(`/wordpress/sites/${domain}/theme/${theme.name}/update`, {
275 method: 'POST'
276 });
277 if (res.ok) {
278 successCount++;
279 showToast(`✓ Updated ${theme.name}`, 'success', 2000);
280 } else {
281 failCount++;
282 showToast(`✗ Failed to update ${theme.name}`, 'error', 2000);
283 }
284 } catch (err) {
285 failCount++;
286 showToast(`✗ Failed to update ${theme.name}`, 'error', 2000);
287 }
288
289 // Small delay to avoid overwhelming the server
290 if (i < themesToUpdate.length - 1) {
291 await new Promise(resolve => setTimeout(resolve, 500));
292 }
293 }
294
295 setBulkUpdateProgress({ current: 0, total: 0, type: '' });
296 setUpdating(prev => ({ ...prev, 'bulk-themes': false }));
297
298 showToast(`Bulk update complete: ${successCount} succeeded, ${failCount} failed`,
299 failCount === 0 ? 'success' : 'warning', 5000);
300
301 fetchSiteDetails();
302 fetchDetailedInfo();
303 };
304
305 const handleAutoLogin = async () => {
306 setUpdating(prev => ({ ...prev, 'autologin': true }));
307 setLoginCredentials(null); // Clear previous credentials
308 try {
309 const res = await apiFetch(`/wordpress/sites/${domain}/autologin`, {
310 method: 'POST',
311 body: JSON.stringify({})
312 });
313 if (res.ok) {
314 const result = await res.json();
315 if (result.method === 'magic_link' && result.login_url) {
316 // Magic link - just open it
317 window.open(result.login_url, '_blank');
318 showToast('Opening WordPress admin...', 'success');
319 } else if (result.method === 'password_reset' && result.password) {
320 // Password reset - store credentials and open login page
321 setLoginCredentials({
322 username: result.username,
323 password: result.password,
324 loginUrl: result.login_url,
325 method: 'password_reset'
326 });
327 window.open(result.login_url, '_blank');
328 showToast('Password reset successful - credentials displayed below', 'success');
329 } else if (result.method === 'manual') {
330 // Manual login required - store username
331 setLoginCredentials({
332 username: result.username,
333 loginUrl: result.login_url,
334 method: 'manual',
335 note: result.note
336 });
337 window.open(result.login_url, '_blank');
338 showToast('Opening login page...', 'info');
339 } else if (result.login_url) {
340 // Generic case - just open the URL
341 window.open(result.login_url, '_blank');
342 showToast('Opening WordPress admin...', 'success');
343 } else {
344 showToast('Auto-login not available for this site', 'warning');
345 }
346 } else {
347 const err = await res.json();
348 showToast(err.error || 'Auto-login failed', 'error');
349 }
350 } catch (err) {
351 showToast('Auto-login failed', 'error');
352 } finally {
353 setUpdating(prev => ({ ...prev, 'autologin': false }));
354 }
355 };
356
357 const getStatusColor = (status) => {
358 const statusMap = {
359 'online': 'success',
360 'error': 'danger',
361 'timeout': 'warning',
362 'offline': 'danger',
363 'unknown': 'secondary'
364 };
365 return statusMap[status?.toLowerCase()] || 'secondary';
366 };
367
368 if (loading) {
369 return (
370 <MainLayout>
371 <div style={{ padding: '40px', textAlign: 'center' }}>
372 <div className="spinner"></div>
373 <p style={{ marginTop: '16px', color: 'var(--text-secondary)' }}>
374 Loading WordPress site details...
375 </p>
376 </div>
377 </MainLayout>
378 );
379 }
380
381 if (!site) {
382 return (
383 <MainLayout>
384 <div style={{ padding: '40px', textAlign: 'center' }}>
385 <span className="material-symbols-outlined" style={{ fontSize: '64px', color: 'var(--text-secondary)', opacity: 0.5 }}>
386 web_asset_off
387 </span>
388 <h2 style={{ marginTop: '16px' }}>Site Not Found</h2>
389 <p style={{ color: 'var(--text-secondary)', marginTop: '8px' }}>
390 The WordPress site "{domain}" could not be found.
391 </p>
392 <button
393 className="btn btn-primary"
394 onClick={() => navigate('/services')}
395 style={{ marginTop: '20px' }}
396 >
397 Back to Services
398 </button>
399 </div>
400 </MainLayout>
401 );
402 }
403
404 return (
405 <MainLayout>
406 <Toast toasts={toasts} hideToast={hideToast} />
407
408 <div className="wordpress-site-detail">
409 {/* Header */}
410 <div className="site-header">
411 <button
412 className="btn btn-secondary"
413 onClick={() => navigate('/services')}
414 style={{ marginBottom: '16px' }}
415 >
416 <span className="material-symbols-outlined">arrow_back</span>
417 Back to Services
418 </button>
419
420 <div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between' }}>
421 <div>
422 <div style={{ display: 'flex', alignItems: 'center', gap: '12px', marginBottom: '8px' }}>
423 <span className="material-symbols-outlined" style={{ fontSize: '32px', color: 'var(--primary)' }}>
424 web
425 </span>
426 <h1 style={{ margin: 0 }}>{site.domain}</h1>
427 <span className={`badge badge-${getStatusColor(site.status)}`}>
428 {site.status || 'unknown'}
429 </span>
430 </div>
431 <a
432 href={`https://${site.domain}`}
433 target="_blank"
434 rel="noopener noreferrer"
435 style={{
436 fontSize: '14px',
437 color: 'var(--primary)',
438 textDecoration: 'none',
439 display: 'flex',
440 alignItems: 'center',
441 gap: '4px',
442 marginLeft: '44px'
443 }}
444 >
445 <span>https://{site.domain}</span>
446 <span className="material-symbols-outlined" style={{ fontSize: '16px' }}>
447 open_in_new
448 </span>
449 </a>
450 </div>
451
452 <div style={{ display: 'flex', gap: '8px' }}>
453 <button className="btn btn-primary" onClick={fetchSiteDetails}>
454 <span className="material-symbols-outlined">refresh</span>
455 Refresh
456 </button>
457 </div>
458 </div>
459 </div>
460
461 {/* Overview Cards */}
462 <div className="overview-grid">
463 <div className="card">
464 <div className="card-header">
465 <span className="material-symbols-outlined">info</span>
466 Site Information
467 </div>
468 <div className="card-body">
469 <div className="info-row">
470 <span>WordPress Version:</span>
471 <span className="mono">{site.wp_version?.trim().replace(/.*\$wp_version\s+/, '') || 'Unknown'}</span>
472 </div>
473 <div className="info-row">
474 <span>Database:</span>
475 <span className="mono small">{site.database || 'N/A'}</span>
476 </div>
477 <div className="info-row">
478 <span>Disk Usage:</span>
479 <span>{site.disk_usage || 'N/A'}</span>
480 </div>
481 <div className="info-row">
482 <span>Path:</span>
483 <span className="mono small">{site.path || 'N/A'}</span>
484 </div>
485 </div>
486 </div>
487
488 <div className="card">
489 <div className="card-header">
490 <span className="material-symbols-outlined">dns</span>
491 Server Information
492 </div>
493 <div className="card-body">
494 <div className="info-row">
495 <span>Server IP:</span>
496 <span className="mono">{site.server_ip || 'N/A'}</span>
497 </div>
498 <div className="info-row">
499 <span>Hostname:</span>
500 <span>{site.server_hostname || 'N/A'}</span>
501 </div>
502 <div className="info-row">
503 <span>Last Checked:</span>
504 <span>{site.last_checked ? new Date(site.last_checked).toLocaleString() : 'N/A'}</span>
505 </div>
506 </div>
507 </div>
508
509 {site.malware_issues > 0 && (
510 <div className="card card-danger">
511 <div className="card-header">
512 <span className="material-symbols-outlined">warning</span>
513 Security Alert
514 </div>
515 <div className="card-body">
516 <p style={{ margin: '0 0 12px 0' }}>
517 <strong>{site.malware_issues}</strong> malware issue{site.malware_issues !== 1 ? 's' : ''} detected
518 </p>
519 {site.malware_details && (
520 <div style={{
521 backgroundColor: 'var(--background-light)',
522 padding: '12px',
523 borderRadius: '6px',
524 marginBottom: '12px',
525 fontSize: '14px',
526 fontFamily: 'monospace',
527 whiteSpace: 'pre-wrap',
528 wordBreak: 'break-word'
529 }}>
530 {site.malware_details}
531 </div>
532 )}
533 <button
534 className="btn btn-danger btn-sm"
535 onClick={handleCleanMalware}
536 disabled={updating.clean}
537 >
538 {updating.clean ? (
539 <>
540 <span className="spinner small"></span>
541 Cleaning...
542 </>
543 ) : (
544 <>
545 <span className="material-symbols-outlined">cleaning_services</span>
546 Clean Malware
547 </>
548 )}
549 </button>
550 </div>
551 </div>
552 )}
553 </div>
554
555 {/* Quick Actions */}
556 <div className="card" style={{ marginTop: '24px' }}>
557 <div className="card-header">
558 <span className="material-symbols-outlined">bolt</span>
559 Quick Actions
560 </div>
561 <div className="card-body">
562 <div style={{ display: 'flex', gap: '12px', flexWrap: 'wrap' }}>
563 <a
564 href={`https://${site.domain}/wp-admin`}
565 target="_blank"
566 rel="noopener noreferrer"
567 className="btn btn-secondary"
568 >
569 <span className="material-symbols-outlined">login</span>
570 WP Admin
571 </a>
572
573 <button
574 className="btn btn-primary"
575 onClick={handleAutoLogin}
576 disabled={updating.autologin}
577 >
578 {updating.autologin ? (
579 <>
580 <span className="spinner small"></span>
581 Generating Link...
582 </>
583 ) : (
584 <>
585 <span className="material-symbols-outlined">key</span>
586 Auto Login
587 </>
588 )}
589 </button>
590 </div>
591
592 {/* Login Credentials Display */}
593 {loginCredentials && (
594 <div style={{
595 marginTop: '16px',
596 padding: '16px',
597 backgroundColor: 'var(--surface-secondary)',
598 border: '1px solid var(--border)',
599 borderRadius: '6px'
600 }}>
601 <div style={{
602 display: 'flex',
603 alignItems: 'center',
604 gap: '8px',
605 marginBottom: '12px',
606 color: 'var(--primary)'
607 }}>
608 <span className="material-symbols-outlined">lock_open</span>
609 <strong>Login Credentials</strong>
610 </div>
611
612 <div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
613 {/* Username */}
614 <div>
615 <label style={{
616 fontSize: '12px',
617 color: 'var(--text-secondary)',
618 display: 'block',
619 marginBottom: '4px'
620 }}>
621 Username
622 </label>
623 <div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
624 <input
625 type="text"
626 value={loginCredentials.username}
627 readOnly
628 style={{
629 flex: 1,
630 padding: '8px 12px',
631 border: '1px solid var(--border)',
632 borderRadius: '4px',
633 fontFamily: 'monospace',
634 fontSize: '14px',
635 backgroundColor: 'var(--surface)',
636 color: 'var(--text-primary)'
637 }}
638 />
639 <button
640 className="btn btn-secondary btn-sm"
641 onClick={() => copyToClipboard(loginCredentials.username, 'Username')}
642 style={{ padding: '8px 12px' }}
643 >
644 <span className="material-symbols-outlined" style={{ fontSize: '18px' }}>
645 content_copy
646 </span>
647 </button>
648 </div>
649 </div>
650
651 {/* Password (if available) */}
652 {loginCredentials.password && (
653 <div>
654 <label style={{
655 fontSize: '12px',
656 color: 'var(--text-secondary)',
657 display: 'block',
658 marginBottom: '4px'
659 }}>
660 Temporary Password
661 </label>
662 <div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
663 <input
664 type="text"
665 value={loginCredentials.password}
666 readOnly
667 style={{
668 flex: 1,
669 padding: '8px 12px',
670 border: '1px solid var(--border)',
671 borderRadius: '4px',
672 fontFamily: 'monospace',
673 fontSize: '14px',
674 backgroundColor: 'var(--surface)',
675 color: 'var(--text-primary)'
676 }}
677 />
678 <button
679 className="btn btn-secondary btn-sm"
680 onClick={() => copyToClipboard(loginCredentials.password, 'Password')}
681 style={{ padding: '8px 12px' }}
682 >
683 <span className="material-symbols-outlined" style={{ fontSize: '18px' }}>
684 content_copy
685 </span>
686 </button>
687 </div>
688 <p style={{
689 fontSize: '12px',
690 color: 'var(--warning)',
691 marginTop: '6px',
692 marginBottom: 0
693 }}>
694 ⚠️ Change this password after logging in
695 </p>
696 </div>
697 )}
698
699 {/* Login URL */}
700 <div>
701 <label style={{
702 fontSize: '12px',
703 color: 'var(--text-secondary)',
704 display: 'block',
705 marginBottom: '4px'
706 }}>
707 Login URL
708 </label>
709 <div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
710 <input
711 type="text"
712 value={loginCredentials.loginUrl}
713 readOnly
714 style={{
715 flex: 1,
716 padding: '8px 12px',
717 border: '1px solid var(--border)',
718 borderRadius: '4px',
719 fontFamily: 'monospace',
720 fontSize: '13px',
721 backgroundColor: 'var(--surface)',
722 color: 'var(--text-primary)'
723 }}
724 />
725 <button
726 className="btn btn-secondary btn-sm"
727 onClick={() => window.open(loginCredentials.loginUrl, '_blank')}
728 style={{ padding: '8px 12px' }}
729 >
730 <span className="material-symbols-outlined" style={{ fontSize: '18px' }}>
731 open_in_new
732 </span>
733 </button>
734 </div>
735 </div>
736
737 {loginCredentials.note && (
738 <p style={{
739 fontSize: '13px',
740 color: 'var(--text-secondary)',
741 margin: 0,
742 fontStyle: 'italic'
743 }}>
744 {loginCredentials.note}
745 </p>
746 )}
747 </div>
748 </div>
749 )}
750 </div>
751 </div>
752
753 {/* Users & Permissions Section */}
754 <div className="card" style={{ marginTop: '16px' }}>
755 <div className="card-header">
756 <span className="material-symbols-outlined">people</span>
757 Users & Permissions ({users.length})
758 </div>
759 <div className="card-body">
760 {usersLoading ? (
761 <p style={{ color: 'var(--text-secondary)', margin: 0 }}>
762 Loading users...
763 </p>
764 ) : users.length === 0 ? (
765 <p style={{ color: 'var(--text-secondary)', margin: 0 }}>
766 No users found on this site.
767 </p>
768 ) : (
769 <div style={{ overflowX: 'auto' }}>
770 <table style={{
771 width: '100%',
772 borderCollapse: 'collapse',
773 fontSize: '14px'
774 }}>
775 <thead>
776 <tr style={{
777 borderBottom: '2px solid var(--border)',
778 textAlign: 'left'
779 }}>
780 <th style={{ padding: '12px 8px', fontWeight: '600' }}>Username</th>
781 <th style={{ padding: '12px 8px', fontWeight: '600' }}>Display Name</th>
782 <th style={{ padding: '12px 8px', fontWeight: '600' }}>Email</th>
783 <th style={{ padding: '12px 8px', fontWeight: '600' }}>Role</th>
784 </tr>
785 </thead>
786 <tbody>
787 {users.map((user) => {
788 const isAdmin = user.roles && user.roles.toLowerCase().includes('administrator');
789 return (
790 <tr
791 key={user.id}
792 style={{
793 borderBottom: '1px solid var(--border)',
794 backgroundColor: isAdmin ? 'var(--warning-bg, rgba(255, 193, 7, 0.1))' : 'transparent'
795 }}
796 >
797 <td style={{ padding: '12px 8px' }}>
798 <div style={{
799 fontWeight: isAdmin ? '600' : '400',
800 display: 'flex',
801 alignItems: 'center',
802 gap: '6px'
803 }}>
804 {isAdmin && (
805 <span className="material-symbols-outlined" style={{
806 fontSize: '16px',
807 color: 'var(--warning)'
808 }}>
809 shield_person
810 </span>
811 )}
812 {user.username}
813 </div>
814 </td>
815 <td style={{ padding: '12px 8px', color: 'var(--text-secondary)' }}>
816 {user.display_name || '-'}
817 </td>
818 <td style={{ padding: '12px 8px', color: 'var(--text-secondary)' }}>
819 {user.email}
820 </td>
821 <td style={{ padding: '12px 8px' }}>
822 <span className={`badge badge-${isAdmin ? 'warning' : 'secondary'}`}>
823 {user.roles || 'none'}
824 </span>
825 </td>
826 </tr>
827 );
828 })}
829 </tbody>
830 </table>
831 </div>
832 )}
833 </div>
834 </div>
835
836 {/* Plugins Section */}
837 <div className="card" style={{ marginTop: '24px' }}>
838 <div className="card-header" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
839 <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
840 <span className="material-symbols-outlined">extension</span>
841 Plugins ({siteDetails?.plugins?.length || site.plugin_count || 0})
842 </div>
843 {siteDetails?.plugins?.some(p => p.update_available === 'available') && (
844 <button
845 className="btn btn-primary btn-sm"
846 onClick={handleUpdateAllPlugins}
847 disabled={updating['bulk-plugins']}
848 >
849 {updating['bulk-plugins'] ? (
850 <>
851 <span className="spinner small"></span>
852 Updating {bulkUpdateProgress.current}/{bulkUpdateProgress.total}...
853 </>
854 ) : (
855 <>
856 <span className="material-symbols-outlined">update</span>
857 Update All ({siteDetails.plugins.filter(p => p.update_available === 'available').length})
858 </>
859 )}
860 </button>
861 )}
862 </div>
863 <div className="card-body">
864 {bulkUpdateProgress.type === 'plugins' && bulkUpdateProgress.total > 0 && (
865 <div style={{
866 marginBottom: '16px',
867 padding: '12px',
868 backgroundColor: 'var(--surface-secondary)',
869 borderRadius: '6px'
870 }}>
871 <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '8px' }}>
872 <span style={{ fontSize: '14px', fontWeight: '500' }}>
873 Updating plugins... ({bulkUpdateProgress.current}/{bulkUpdateProgress.total})
874 </span>
875 <span style={{ fontSize: '14px', color: 'var(--text-secondary)' }}>
876 {Math.round((bulkUpdateProgress.current / bulkUpdateProgress.total) * 100)}%
877 </span>
878 </div>
879 <div style={{
880 width: '100%',
881 height: '6px',
882 backgroundColor: 'var(--border)',
883 borderRadius: '3px',
884 overflow: 'hidden'
885 }}>
886 <div style={{
887 width: `${(bulkUpdateProgress.current / bulkUpdateProgress.total) * 100}%`,
888 height: '100%',
889 backgroundColor: 'var(--primary)',
890 transition: 'width 0.3s ease'
891 }} />
892 </div>
893 </div>
894 )}
895 {detailsLoading ? (
896 <p style={{ color: 'var(--text-secondary)', margin: 0 }}>
897 Loading plugin information...
898 </p>
899 ) : !siteDetails || !siteDetails.plugins || siteDetails.plugins.length === 0 ? (
900 <p style={{ color: 'var(--text-secondary)', margin: 0 }}>
901 No plugins found on this site.
902 </p>
903 ) : (
904 <div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
905 {siteDetails.plugins.map((plugin) => (
906 <div key={plugin.name} style={{
907 display: 'flex',
908 justifyContent: 'space-between',
909 alignItems: 'center',
910 padding: '12px',
911 border: '1px solid var(--border)',
912 borderRadius: '4px'
913 }}>
914 <div>
915 <div style={{ fontWeight: '500' }}>{plugin.title || plugin.name}</div>
916 <div style={{ fontSize: '13px', color: 'var(--text-secondary)', marginTop: '4px' }}>
917 Version: {plugin.version}
918 {plugin.update_available === 'available' && plugin.new_version && (
919 <span style={{ color: 'var(--warning)', marginLeft: '8px' }}>
920 → Update to {plugin.new_version}
921 </span>
922 )}
923 <span
924 className={`badge badge-${plugin.status === 'active' ? 'success' : 'secondary'}`}
925 style={{ marginLeft: '8px' }}
926 >
927 {plugin.status}
928 </span>
929 </div>
930 </div>
931 {plugin.update_available === 'available' && (
932 <button
933 className="btn btn-primary btn-sm"
934 onClick={() => handlePluginUpdate(plugin.name)}
935 disabled={updating[`plugin-${plugin.name}`]}
936 >
937 {updating[`plugin-${plugin.name}`] ? (
938 <>
939 <span className="spinner small"></span>
940 Updating...
941 </>
942 ) : (
943 <>
944 <span className="material-symbols-outlined">update</span>
945 Update
946 </>
947 )}
948 </button>
949 )}
950 </div>
951 ))}
952 </div>
953 )}
954 </div>
955 </div>
956
957 {/* Themes Section */}
958 <div className="card" style={{ marginTop: '16px' }}>
959 <div className="card-header" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
960 <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
961 <span className="material-symbols-outlined">palette</span>
962 Themes ({siteDetails?.themes?.length || 0})
963 </div>
964 {siteDetails?.themes?.some(t => t.update_available === 'available') && (
965 <button
966 className="btn btn-primary btn-sm"
967 onClick={handleUpdateAllThemes}
968 disabled={updating['bulk-themes']}
969 >
970 {updating['bulk-themes'] ? (
971 <>
972 <span className="spinner small"></span>
973 Updating {bulkUpdateProgress.current}/{bulkUpdateProgress.total}...
974 </>
975 ) : (
976 <>
977 <span className="material-symbols-outlined">update</span>
978 Update All ({siteDetails.themes.filter(t => t.update_available === 'available').length})
979 </>
980 )}
981 </button>
982 )}
983 </div>
984 <div className="card-body">
985 {bulkUpdateProgress.type === 'themes' && bulkUpdateProgress.total > 0 && (
986 <div style={{
987 marginBottom: '16px',
988 padding: '12px',
989 backgroundColor: 'var(--surface-secondary)',
990 borderRadius: '6px'
991 }}>
992 <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '8px' }}>
993 <span style={{ fontSize: '14px', fontWeight: '500' }}>
994 Updating themes... ({bulkUpdateProgress.current}/{bulkUpdateProgress.total})
995 </span>
996 <span style={{ fontSize: '14px', color: 'var(--text-secondary)' }}>
997 {Math.round((bulkUpdateProgress.current / bulkUpdateProgress.total) * 100)}%
998 </span>
999 </div>
1000 <div style={{
1001 width: '100%',
1002 height: '6px',
1003 backgroundColor: 'var(--border)',
1004 borderRadius: '3px',
1005 overflow: 'hidden'
1006 }}>
1007 <div style={{
1008 width: `${(bulkUpdateProgress.current / bulkUpdateProgress.total) * 100}%`,
1009 height: '100%',
1010 backgroundColor: 'var(--primary)',
1011 transition: 'width 0.3s ease'
1012 }} />
1013 </div>
1014 </div>
1015 )}
1016 {detailsLoading ? (
1017 <p style={{ color: 'var(--text-secondary)', margin: 0 }}>
1018 Loading theme information...
1019 </p>
1020 ) : !siteDetails || !siteDetails.themes || siteDetails.themes.length === 0 ? (
1021 <p style={{ color: 'var(--text-secondary)', margin: 0 }}>
1022 No themes found on this site.
1023 </p>
1024 ) : (
1025 <div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
1026 {siteDetails.themes.map((theme) => (
1027 <div key={theme.name} style={{
1028 display: 'flex',
1029 justifyContent: 'space-between',
1030 alignItems: 'center',
1031 padding: '12px',
1032 border: '1px solid var(--border)',
1033 borderRadius: '4px'
1034 }}>
1035 <div>
1036 <div style={{ fontWeight: '500' }}>{theme.name}</div>
1037 <div style={{ fontSize: '13px', color: 'var(--text-secondary)', marginTop: '4px' }}>
1038 Version: {theme.version}
1039 {theme.update_available === 'available' && theme.new_version && (
1040 <span style={{ color: 'var(--warning)', marginLeft: '8px' }}>
1041 → Update to {theme.new_version}
1042 </span>
1043 )}
1044 <span
1045 className={`badge badge-${theme.status === 'active' ? 'success' : 'secondary'}`}
1046 style={{ marginLeft: '8px' }}
1047 >
1048 {theme.status}
1049 </span>
1050 </div>
1051 </div>
1052 {theme.update_available === 'available' && (
1053 <button
1054 className="btn btn-primary btn-sm"
1055 onClick={() => handleThemeUpdate(theme.name)}
1056 disabled={updating[`theme-${theme.name}`]}
1057 >
1058 {updating[`theme-${theme.name}`] ? (
1059 <>
1060 <span className="spinner small"></span>
1061 Updating...
1062 </>
1063 ) : (
1064 <>
1065 <span className="material-symbols-outlined">update</span>
1066 Update
1067 </>
1068 )}
1069 </button>
1070 )}
1071 </div>
1072 ))}
1073 </div>
1074 )}
1075 </div>
1076 </div>
1077
1078 {/* WordPress Core Section */}
1079 {siteDetails && siteDetails.core && (
1080 <div className="card" style={{ marginTop: '16px' }}>
1081 <div className="card-header">
1082 <span className="material-symbols-outlined">system_update</span>
1083 WordPress Core
1084 </div>
1085 <div className="card-body">
1086 <div style={{
1087 display: 'flex',
1088 justifyContent: 'space-between',
1089 alignItems: 'center'
1090 }}>
1091 <div>
1092 <div style={{ fontWeight: '500' }}>WordPress {siteDetails.core.version}</div>
1093 {siteDetails.core.update_available && siteDetails.core.updates && siteDetails.core.updates.length > 0 && (
1094 <div style={{ fontSize: '13px', color: 'var(--warning)', marginTop: '4px' }}>
1095 Update available: {siteDetails.core.updates[0].version}
1096 </div>
1097 )}
1098 {!siteDetails.core.update_available && (
1099 <div style={{ fontSize: '13px', color: 'var(--success)', marginTop: '4px' }}>
1100 ✓ Up to date
1101 </div>
1102 )}
1103 </div>
1104 {siteDetails.core.update_available && siteDetails.core.updates && siteDetails.core.updates.length > 0 && (
1105 <button
1106 className="btn btn-primary btn-sm"
1107 onClick={handleCoreUpdate}
1108 disabled={updating.core}
1109 >
1110 {updating.core ? (
1111 <>
1112 <span className="spinner small"></span>
1113 Updating...
1114 </>
1115 ) : (
1116 <>
1117 <span className="material-symbols-outlined">update</span>
1118 Update to {siteDetails.core.updates[0].version}
1119 </>
1120 )}
1121 </button>
1122 )}
1123 </div>
1124 </div>
1125 </div>
1126 )}
1127 </div>
1128 </MainLayout>
1129 );
1130}