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";
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: '' });
30 // Refresh data every 30 seconds
31 const interval = setInterval(() => {
36 return () => clearInterval(interval);
39 const fetchSiteDetails = async () => {
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);
52 server_ip: latestReport.server_ip,
53 server_hostname: latestReport.server_hostname,
54 last_checked: latestReport.timestamp,
55 last_scan: latestReport.last_scan
61 console.error('Error fetching site details:', err);
62 showToast('Failed to load site details', 'error');
68 const fetchDetailedInfo = async () => {
70 const res = await apiFetch(`/wordpress/sites/${domain}/details`);
72 const data = await res.json();
76 console.error('Error fetching detailed site info:', err);
78 setDetailsLoading(false);
82 const fetchUsers = async () => {
84 const res = await apiFetch(`/wordpress/sites/${domain}/users`);
86 const data = await res.json();
87 setUsers(data.users || []);
90 console.error('Error fetching users:', err);
92 setUsersLoading(false);
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);
102 const hideToast = (id) => {
103 setToasts(prev => prev.filter(t => t.id !== id));
106 const copyToClipboard = (text, label) => {
107 navigator.clipboard.writeText(text).then(() => {
108 showToast(`${label} copied to clipboard`, 'success', 2000);
110 showToast('Failed to copy', 'error');
114 const handlePluginUpdate = async (plugin) => {
115 setUpdating(prev => ({ ...prev, [`plugin-${plugin}`]: true }));
117 const res = await apiFetch(`/wordpress/sites/${domain}/plugin/${plugin}/update`, {
121 showToast(`Plugin ${plugin} updated successfully`, 'success');
125 const err = await res.json();
126 showToast(err.error || 'Update failed', 'error');
129 showToast('Update failed', 'error');
131 setUpdating(prev => ({ ...prev, [`plugin-${plugin}`]: false }));
135 const handleThemeUpdate = async (theme) => {
136 setUpdating(prev => ({ ...prev, [`theme-${theme}`]: true }));
138 const res = await apiFetch(`/wordpress/sites/${domain}/theme/${theme}/update`, {
142 showToast(`Theme ${theme} updated successfully`, 'success');
146 const err = await res.json();
147 showToast(err.error || 'Update failed', 'error');
150 showToast('Update failed', 'error');
152 setUpdating(prev => ({ ...prev, [`theme-${theme}`]: false }));
156 const handleCoreUpdate = async () => {
157 setUpdating(prev => ({ ...prev, 'core': true }));
159 const res = await apiFetch(`/wordpress/sites/${domain}/core/update`, {
163 showToast('WordPress core updated successfully', 'success');
167 const err = await res.json();
168 showToast(err.error || 'Update failed', 'error');
171 showToast('Update failed', 'error');
173 setUpdating(prev => ({ ...prev, 'core': false }));
177 const handleCleanMalware = async () => {
178 if (!confirm('This will remove detected malicious files. Continue?')) return;
180 setUpdating(prev => ({ ...prev, 'clean': true }));
182 const res = await apiFetch(`/wordpress/sites/${domain}/clean`, {
186 const result = await res.json();
187 showToast(`Cleanup complete: ${result.files_removed || 0} files removed`, 'success');
191 const err = await res.json();
192 showToast(err.error || 'Cleanup failed', 'error');
195 showToast('Cleanup failed', 'error');
197 setUpdating(prev => ({ ...prev, 'clean': false }));
201 const handleUpdateAllPlugins = async () => {
202 const pluginsToUpdate = siteDetails?.plugins?.filter(p => p.update_available === 'available') || [];
204 if (pluginsToUpdate.length === 0) {
205 showToast('No plugin updates available', 'info');
209 if (!confirm(`Update ${pluginsToUpdate.length} plugin(s)? This may take several minutes.`)) return;
211 setBulkUpdateProgress({ current: 0, total: pluginsToUpdate.length, type: 'plugins' });
212 setUpdating(prev => ({ ...prev, 'bulk-plugins': true }));
214 let successCount = 0;
217 for (let i = 0; i < pluginsToUpdate.length; i++) {
218 const plugin = pluginsToUpdate[i];
219 setBulkUpdateProgress({ current: i + 1, total: pluginsToUpdate.length, type: 'plugins' });
222 const res = await apiFetch(`/wordpress/sites/${domain}/plugin/${plugin.name}/update`, {
227 showToast(`✓ Updated ${plugin.title || plugin.name}`, 'success', 2000);
230 showToast(`✗ Failed to update ${plugin.title || plugin.name}`, 'error', 2000);
234 showToast(`✗ Failed to update ${plugin.title || plugin.name}`, 'error', 2000);
237 // Small delay to avoid overwhelming the server
238 if (i < pluginsToUpdate.length - 1) {
239 await new Promise(resolve => setTimeout(resolve, 500));
243 setBulkUpdateProgress({ current: 0, total: 0, type: '' });
244 setUpdating(prev => ({ ...prev, 'bulk-plugins': false }));
246 showToast(`Bulk update complete: ${successCount} succeeded, ${failCount} failed`,
247 failCount === 0 ? 'success' : 'warning', 5000);
253 const handleUpdateAllThemes = async () => {
254 const themesToUpdate = siteDetails?.themes?.filter(t => t.update_available === 'available') || [];
256 if (themesToUpdate.length === 0) {
257 showToast('No theme updates available', 'info');
261 if (!confirm(`Update ${themesToUpdate.length} theme(s)? This may take several minutes.`)) return;
263 setBulkUpdateProgress({ current: 0, total: themesToUpdate.length, type: 'themes' });
264 setUpdating(prev => ({ ...prev, 'bulk-themes': true }));
266 let successCount = 0;
269 for (let i = 0; i < themesToUpdate.length; i++) {
270 const theme = themesToUpdate[i];
271 setBulkUpdateProgress({ current: i + 1, total: themesToUpdate.length, type: 'themes' });
274 const res = await apiFetch(`/wordpress/sites/${domain}/theme/${theme.name}/update`, {
279 showToast(`✓ Updated ${theme.name}`, 'success', 2000);
282 showToast(`✗ Failed to update ${theme.name}`, 'error', 2000);
286 showToast(`✗ Failed to update ${theme.name}`, 'error', 2000);
289 // Small delay to avoid overwhelming the server
290 if (i < themesToUpdate.length - 1) {
291 await new Promise(resolve => setTimeout(resolve, 500));
295 setBulkUpdateProgress({ current: 0, total: 0, type: '' });
296 setUpdating(prev => ({ ...prev, 'bulk-themes': false }));
298 showToast(`Bulk update complete: ${successCount} succeeded, ${failCount} failed`,
299 failCount === 0 ? 'success' : 'warning', 5000);
305 const handleAutoLogin = async () => {
306 setUpdating(prev => ({ ...prev, 'autologin': true }));
307 setLoginCredentials(null); // Clear previous credentials
309 const res = await apiFetch(`/wordpress/sites/${domain}/autologin`, {
311 body: JSON.stringify({})
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'
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,
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');
344 showToast('Auto-login not available for this site', 'warning');
347 const err = await res.json();
348 showToast(err.error || 'Auto-login failed', 'error');
351 showToast('Auto-login failed', 'error');
353 setUpdating(prev => ({ ...prev, 'autologin': false }));
357 const getStatusColor = (status) => {
361 'timeout': 'warning',
363 'unknown': 'secondary'
365 return statusMap[status?.toLowerCase()] || 'secondary';
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...
384 <div style={{ padding: '40px', textAlign: 'center' }}>
385 <span className="material-symbols-outlined" style={{ fontSize: '64px', color: 'var(--text-secondary)', opacity: 0.5 }}>
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.
393 className="btn btn-primary"
394 onClick={() => navigate('/services')}
395 style={{ marginTop: '20px' }}
406 <Toast toasts={toasts} hideToast={hideToast} />
408 <div className="wordpress-site-detail">
410 <div className="site-header">
412 className="btn btn-secondary"
413 onClick={() => navigate('/services')}
414 style={{ marginBottom: '16px' }}
416 <span className="material-symbols-outlined">arrow_back</span>
420 <div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between' }}>
422 <div style={{ display: 'flex', alignItems: 'center', gap: '12px', marginBottom: '8px' }}>
423 <span className="material-symbols-outlined" style={{ fontSize: '32px', color: 'var(--primary)' }}>
426 <h1 style={{ margin: 0 }}>{site.domain}</h1>
427 <span className={`badge badge-${getStatusColor(site.status)}`}>
428 {site.status || 'unknown'}
432 href={`https://${site.domain}`}
434 rel="noopener noreferrer"
437 color: 'var(--primary)',
438 textDecoration: 'none',
440 alignItems: 'center',
445 <span>https://{site.domain}</span>
446 <span className="material-symbols-outlined" style={{ fontSize: '16px' }}>
452 <div style={{ display: 'flex', gap: '8px' }}>
453 <button className="btn btn-primary" onClick={fetchSiteDetails}>
454 <span className="material-symbols-outlined">refresh</span>
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>
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>
473 <div className="info-row">
474 <span>Database:</span>
475 <span className="mono small">{site.database || 'N/A'}</span>
477 <div className="info-row">
478 <span>Disk Usage:</span>
479 <span>{site.disk_usage || 'N/A'}</span>
481 <div className="info-row">
483 <span className="mono small">{site.path || 'N/A'}</span>
488 <div className="card">
489 <div className="card-header">
490 <span className="material-symbols-outlined">dns</span>
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>
498 <div className="info-row">
499 <span>Hostname:</span>
500 <span>{site.server_hostname || 'N/A'}</span>
502 <div className="info-row">
503 <span>Last Checked:</span>
504 <span>{site.last_checked ? new Date(site.last_checked).toLocaleString() : 'N/A'}</span>
509 {site.malware_issues > 0 && (
510 <div className="card card-danger">
511 <div className="card-header">
512 <span className="material-symbols-outlined">warning</span>
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
519 {site.malware_details && (
521 backgroundColor: 'var(--background-light)',
524 marginBottom: '12px',
526 fontFamily: 'monospace',
527 whiteSpace: 'pre-wrap',
528 wordBreak: 'break-word'
530 {site.malware_details}
534 className="btn btn-danger btn-sm"
535 onClick={handleCleanMalware}
536 disabled={updating.clean}
540 <span className="spinner small"></span>
545 <span className="material-symbols-outlined">cleaning_services</span>
555 {/* Quick Actions */}
556 <div className="card" style={{ marginTop: '24px' }}>
557 <div className="card-header">
558 <span className="material-symbols-outlined">bolt</span>
561 <div className="card-body">
562 <div style={{ display: 'flex', gap: '12px', flexWrap: 'wrap' }}>
564 href={`https://${site.domain}/wp-admin`}
566 rel="noopener noreferrer"
567 className="btn btn-secondary"
569 <span className="material-symbols-outlined">login</span>
574 className="btn btn-primary"
575 onClick={handleAutoLogin}
576 disabled={updating.autologin}
578 {updating.autologin ? (
580 <span className="spinner small"></span>
585 <span className="material-symbols-outlined">key</span>
592 {/* Login Credentials Display */}
593 {loginCredentials && (
597 backgroundColor: 'var(--surface-secondary)',
598 border: '1px solid var(--border)',
603 alignItems: 'center',
605 marginBottom: '12px',
606 color: 'var(--primary)'
608 <span className="material-symbols-outlined">lock_open</span>
609 <strong>Login Credentials</strong>
612 <div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
617 color: 'var(--text-secondary)',
623 <div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
626 value={loginCredentials.username}
631 border: '1px solid var(--border)',
633 fontFamily: 'monospace',
635 backgroundColor: 'var(--surface)',
636 color: 'var(--text-primary)'
640 className="btn btn-secondary btn-sm"
641 onClick={() => copyToClipboard(loginCredentials.username, 'Username')}
642 style={{ padding: '8px 12px' }}
644 <span className="material-symbols-outlined" style={{ fontSize: '18px' }}>
651 {/* Password (if available) */}
652 {loginCredentials.password && (
656 color: 'var(--text-secondary)',
662 <div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
665 value={loginCredentials.password}
670 border: '1px solid var(--border)',
672 fontFamily: 'monospace',
674 backgroundColor: 'var(--surface)',
675 color: 'var(--text-primary)'
679 className="btn btn-secondary btn-sm"
680 onClick={() => copyToClipboard(loginCredentials.password, 'Password')}
681 style={{ padding: '8px 12px' }}
683 <span className="material-symbols-outlined" style={{ fontSize: '18px' }}>
690 color: 'var(--warning)',
694 ⚠️ Change this password after logging in
703 color: 'var(--text-secondary)',
709 <div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
712 value={loginCredentials.loginUrl}
717 border: '1px solid var(--border)',
719 fontFamily: 'monospace',
721 backgroundColor: 'var(--surface)',
722 color: 'var(--text-primary)'
726 className="btn btn-secondary btn-sm"
727 onClick={() => window.open(loginCredentials.loginUrl, '_blank')}
728 style={{ padding: '8px 12px' }}
730 <span className="material-symbols-outlined" style={{ fontSize: '18px' }}>
737 {loginCredentials.note && (
740 color: 'var(--text-secondary)',
744 {loginCredentials.note}
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})
759 <div className="card-body">
761 <p style={{ color: 'var(--text-secondary)', margin: 0 }}>
764 ) : users.length === 0 ? (
765 <p style={{ color: 'var(--text-secondary)', margin: 0 }}>
766 No users found on this site.
769 <div style={{ overflowX: 'auto' }}>
772 borderCollapse: 'collapse',
777 borderBottom: '2px solid var(--border)',
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>
787 {users.map((user) => {
788 const isAdmin = user.roles && user.roles.toLowerCase().includes('administrator');
793 borderBottom: '1px solid var(--border)',
794 backgroundColor: isAdmin ? 'var(--warning-bg, rgba(255, 193, 7, 0.1))' : 'transparent'
797 <td style={{ padding: '12px 8px' }}>
799 fontWeight: isAdmin ? '600' : '400',
801 alignItems: 'center',
805 <span className="material-symbols-outlined" style={{
807 color: 'var(--warning)'
815 <td style={{ padding: '12px 8px', color: 'var(--text-secondary)' }}>
816 {user.display_name || '-'}
818 <td style={{ padding: '12px 8px', color: 'var(--text-secondary)' }}>
821 <td style={{ padding: '12px 8px' }}>
822 <span className={`badge badge-${isAdmin ? 'warning' : 'secondary'}`}>
823 {user.roles || 'none'}
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})
843 {siteDetails?.plugins?.some(p => p.update_available === 'available') && (
845 className="btn btn-primary btn-sm"
846 onClick={handleUpdateAllPlugins}
847 disabled={updating['bulk-plugins']}
849 {updating['bulk-plugins'] ? (
851 <span className="spinner small"></span>
852 Updating {bulkUpdateProgress.current}/{bulkUpdateProgress.total}...
856 <span className="material-symbols-outlined">update</span>
857 Update All ({siteDetails.plugins.filter(p => p.update_available === 'available').length})
863 <div className="card-body">
864 {bulkUpdateProgress.type === 'plugins' && bulkUpdateProgress.total > 0 && (
866 marginBottom: '16px',
868 backgroundColor: 'var(--surface-secondary)',
871 <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '8px' }}>
872 <span style={{ fontSize: '14px', fontWeight: '500' }}>
873 Updating plugins... ({bulkUpdateProgress.current}/{bulkUpdateProgress.total})
875 <span style={{ fontSize: '14px', color: 'var(--text-secondary)' }}>
876 {Math.round((bulkUpdateProgress.current / bulkUpdateProgress.total) * 100)}%
882 backgroundColor: 'var(--border)',
887 width: `${(bulkUpdateProgress.current / bulkUpdateProgress.total) * 100}%`,
889 backgroundColor: 'var(--primary)',
890 transition: 'width 0.3s ease'
896 <p style={{ color: 'var(--text-secondary)', margin: 0 }}>
897 Loading plugin information...
899 ) : !siteDetails || !siteDetails.plugins || siteDetails.plugins.length === 0 ? (
900 <p style={{ color: 'var(--text-secondary)', margin: 0 }}>
901 No plugins found on this site.
904 <div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
905 {siteDetails.plugins.map((plugin) => (
906 <div key={plugin.name} style={{
908 justifyContent: 'space-between',
909 alignItems: 'center',
911 border: '1px solid var(--border)',
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}
924 className={`badge badge-${plugin.status === 'active' ? 'success' : 'secondary'}`}
925 style={{ marginLeft: '8px' }}
931 {plugin.update_available === 'available' && (
933 className="btn btn-primary btn-sm"
934 onClick={() => handlePluginUpdate(plugin.name)}
935 disabled={updating[`plugin-${plugin.name}`]}
937 {updating[`plugin-${plugin.name}`] ? (
939 <span className="spinner small"></span>
944 <span className="material-symbols-outlined">update</span>
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})
964 {siteDetails?.themes?.some(t => t.update_available === 'available') && (
966 className="btn btn-primary btn-sm"
967 onClick={handleUpdateAllThemes}
968 disabled={updating['bulk-themes']}
970 {updating['bulk-themes'] ? (
972 <span className="spinner small"></span>
973 Updating {bulkUpdateProgress.current}/{bulkUpdateProgress.total}...
977 <span className="material-symbols-outlined">update</span>
978 Update All ({siteDetails.themes.filter(t => t.update_available === 'available').length})
984 <div className="card-body">
985 {bulkUpdateProgress.type === 'themes' && bulkUpdateProgress.total > 0 && (
987 marginBottom: '16px',
989 backgroundColor: 'var(--surface-secondary)',
992 <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '8px' }}>
993 <span style={{ fontSize: '14px', fontWeight: '500' }}>
994 Updating themes... ({bulkUpdateProgress.current}/{bulkUpdateProgress.total})
996 <span style={{ fontSize: '14px', color: 'var(--text-secondary)' }}>
997 {Math.round((bulkUpdateProgress.current / bulkUpdateProgress.total) * 100)}%
1003 backgroundColor: 'var(--border)',
1004 borderRadius: '3px',
1008 width: `${(bulkUpdateProgress.current / bulkUpdateProgress.total) * 100}%`,
1010 backgroundColor: 'var(--primary)',
1011 transition: 'width 0.3s ease'
1017 <p style={{ color: 'var(--text-secondary)', margin: 0 }}>
1018 Loading theme information...
1020 ) : !siteDetails || !siteDetails.themes || siteDetails.themes.length === 0 ? (
1021 <p style={{ color: 'var(--text-secondary)', margin: 0 }}>
1022 No themes found on this site.
1025 <div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
1026 {siteDetails.themes.map((theme) => (
1027 <div key={theme.name} style={{
1029 justifyContent: 'space-between',
1030 alignItems: 'center',
1032 border: '1px solid var(--border)',
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}
1045 className={`badge badge-${theme.status === 'active' ? 'success' : 'secondary'}`}
1046 style={{ marginLeft: '8px' }}
1052 {theme.update_available === 'available' && (
1054 className="btn btn-primary btn-sm"
1055 onClick={() => handleThemeUpdate(theme.name)}
1056 disabled={updating[`theme-${theme.name}`]}
1058 {updating[`theme-${theme.name}`] ? (
1060 <span className="spinner small"></span>
1065 <span className="material-symbols-outlined">update</span>
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>
1085 <div className="card-body">
1088 justifyContent: 'space-between',
1089 alignItems: 'center'
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}
1098 {!siteDetails.core.update_available && (
1099 <div style={{ fontSize: '13px', color: 'var(--success)', marginTop: '4px' }}>
1104 {siteDetails.core.update_available && siteDetails.core.updates && siteDetails.core.updates.length > 0 && (
1106 className="btn btn-primary btn-sm"
1107 onClick={handleCoreUpdate}
1108 disabled={updating.core}
1112 <span className="spinner small"></span>
1117 <span className="material-symbols-outlined">update</span>
1118 Update to {siteDetails.core.updates[0].version}