EverydayTech Platform - Developer Reference
Complete Source Code Documentation - All Applications
Loading...
Searching...
No Matches
TabHosting-old.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
26 const fetchWordPressSites = async () => {
27 setLoading(true);
28 setError('');
29 try {
30 // Fetch monitoring data for self-hosted sites
31 const monitoringRes = await apiFetch('/wordpress/monitoring/latest');
32 if (monitoringRes.ok) {
33 const monitoringData = await monitoringRes.json();
34 if (monitoringData.reports && monitoringData.reports.length > 0) {
35 const latestReport = monitoringData.reports[0];
36 if (latestReport && latestReport.sites) {
37 const sites = latestReport.sites.map(site => ({
38 ...site,
39 name: site.domain,
40 server_ip: latestReport.server_ip,
41 server_hostname: latestReport.server_hostname,
42 last_checked: latestReport.timestamp,
43 last_scan: latestReport.last_scan
44 }));
45 setWordPressSites(sites);
46 setLastUpdate(latestReport.received_at || latestReport.timestamp);
47 }
48 }
49 } else {
50 setError('Failed to fetch WordPress sites');
51 }
52 } catch (err) {
53 console.error('Error fetching WordPress sites:', err);
54 setError(err.message || 'Failed to load WordPress sites');
55 } finally {
56 setLoading(false);
57 }
58 };
59
60 const fetchCustomers = async () => {
61 try {
62 const res = await apiFetch('/customers');
63 if (res.ok) {
64 const data = await res.json();
65 setCustomers(data.customers || []);
66 }
67 } catch (err) {
68 console.error('Error fetching customers:', err);
69 }
70 };
71
72 const handleAssignSite = (site) => {
73 setEditingSite(site);
74 setAssignmentData({
75 customerId: site.customer_id || '',
76 notes: site.notes || ''
77 });
78 };
79
80 const handleSaveAssignment = async () => {
81 if (!editingSite) return;
82
83 try {
84 const res = await apiFetch(`/wordpress/sites/${editingSite.domain}/assign`, {
85 method: 'POST',
86 headers: { 'Content-Type': 'application/json' },
87 body: JSON.stringify(assignmentData)
88 });
89
90 if (res.ok) {
91 showToast('Site assignment updated successfully', 'success', 3000);
92 setEditingSite(null);
93 fetchWordPressSites();
94 } else {
95 const errorData = await res.json();
96 showToast(errorData.error || 'Failed to update assignment', 'error', 5000);
97 }
98 } catch (err) {
99 console.error('Error saving assignment:', err);
100 showToast(err.message || 'Failed to save assignment', 'error', 5000);
101 }
102 };
103
104 const getStatusColor = (status) => {
105 const statusMap = {
106 'online': 'success',
107 'error': 'danger',
108 'timeout': 'warning',
109 'offline': 'danger',
110 'unknown': 'secondary'
111 };
112 return statusMap[status?.toLowerCase()] || 'secondary';
113 };
114
115 const handleRefresh = () => {
116 fetchWordPressSites();
117 showToast('Refreshing WordPress sites...', 'info', 2000);
118 };
119 } else {
120 const errorData = await res.json();
121 setError(errorData.error || 'Failed to organize apps');
122 }
123 } catch (err) {
124 console.error('Error organizing apps:', err);
125 setError(err.message || 'Failed to organize apps');
126 } finally {
127 setOrganizing(false);
128 }
129 };
130
131 const handleWordPressInputChange = (e) => {
132 const { name, value } = e.target;
133 setWordpressFormData(prev => ({
134 ...prev,
135 [name]: value
136 }));
137 };
138
139 const handleWordPressSubmit = async (e) => {
140 e.preventDefault();
141 setCreatingWordPress(true);
142 setError('');
143 showToast(
144 'Creating WordPress site... This may take a few minutes. Please wait.',
145 'info',
146 10000
147 );
148 try {
149 const res = await apiFetch('/wordpress/create', {
150 method: 'POST',
151 headers: { 'Content-Type': 'application/json' },
152 body: JSON.stringify(wordpressFormData)
153 });
154 if (res.ok) {
155 const data = await res.json();
156 setWpDeploymentId(data.deploymentId || null);
157 setWpProgressVisible(true);
158 showToast(
159 `WordPress site "${wordpressFormData.siteName}" is being deployed! This can take 5-10 minutes.`,
160 'success',
161 15000
162 );
163 setShowWordPressForm(false);
164 setWordpressFormData({ siteName: '', customerId: '', domain: '' });
165 setTimeout(() => {
166 fetchWordPressSites();
167 fetchApps();
168 }, 2000);
169 } else {
170 const errorData = await res.json();
171 const errorMessage = errorData.error || 'Failed to create WordPress site';
172 setError(errorMessage);
173 showToast(errorMessage, 'error', 10000);
174 }
175 } catch (err) {
176 console.error('Error creating WordPress site:', err);
177 const errorMessage = err.message || 'Failed to create WordPress site';
178 setError(errorMessage);
179 showToast(errorMessage, 'error', 10000);
180 } finally {
181 setCreatingWordPress(false);
182 }
183 };
184
185 const handleCreateClick = (type) => {
186 setShowCreateDropdown(false);
187 if (type === 'wordpress') {
188 setShowWordPressForm(true);
189 } else if (type === 'nodejs') {
190 alert('Node.js app creation coming soon!');
191 }
192 };
193
194 const getStatusColor = (status) => {
195 const statusMap = {
196 'running': 'success',
197 'deploying': 'info',
198 'error': 'error',
199 'stopped': 'secondary'
200 };
201 return statusMap[status?.toLowerCase()] || 'secondary';
202 };
203
204 const formatTime = (date) => {
205 if (!date) return '';
206 const now = new Date();
207 const then = new Date(date);
208 const diff = Math.floor((now - then) / 1000);
209
210 if (diff < 60) return `${diff} seconds ago`;
211 if (diff < 3600) return `${Math.floor(diff / 60)} minutes ago`;
212 if (diff < 86400) return `${Math.floor(diff / 3600)} hours ago`;
213 return `${Math.floor(diff / 86400)} days ago`;
214 };
215
216 if (loading) {
217 return (
218 <div style={{ padding: '40px', textAlign: 'center' }}>
219 <div className="spinner"></div>
220 </div>
221 );
222 }
223
224 return (
225 <>
226 {/* WordPress Deployment Progress Modal */}
227 {wpProgressVisible && wpProgress && (
228 <div style={{ position: 'fixed', top: 80, left: 0, right: 0, zIndex: 2000, display: 'flex', justifyContent: 'center' }}>
229 <div style={{ background: '#fff', border: '1px solid #ccc', borderRadius: 8, padding: 24, minWidth: 340, boxShadow: '0 4px 16px rgba(0,0,0,0.12)' }}>
230 <h4 style={{ marginBottom: 12 }}>WordPress Deployment Progress</h4>
231 <div style={{ marginBottom: 8 }}>
232 <b>Step:</b> {wpProgress.step}
233 </div>
234 <div style={{ marginBottom: 8 }}>
235 <b>Status:</b> {wpProgress.status}
236 </div>
237 <div style={{ marginBottom: 8 }}>
238 <b>Message:</b> {wpProgress.message}
239 </div>
240 {wpProgress.error && (
241 <div style={{ color: 'red', marginBottom: 8 }}>
242 <b>Error:</b> {wpProgress.error}
243 {wpProgress.errorLog && (
244 <pre style={{
245 background: '#fff0f0',
246 color: '#b71c1c',
247 padding: '10px',
248 borderRadius: '6px',
249 marginTop: '8px',
250 fontSize: '12px',
251 maxHeight: '180px',
252 overflow: 'auto',
253 whiteSpace: 'pre-wrap',
254 wordBreak: 'break-all'
255 }}>{wpProgress.errorLog}</pre>
256 )}
257 </div>
258 )}
259 <div style={{ height: 8, background: '#eee', borderRadius: 4, margin: '16px 0' }}>
260 <div style={{
261 width: wpProgress.status === 'success' ? '100%' : wpProgress.status === 'error' ? '100%' :
262 wpProgress.step === 'starting' ? '5%' :
263 wpProgress.step === 'credentials' ? '15%' :
264 wpProgress.step === 'database' ? '35%' :
265 wpProgress.step === 'spaces' ? '50%' :
266 wpProgress.step === 'project' ? '65%' :
267 wpProgress.step === 'app' ? '85%' : '90%',
268 background: wpProgress.status === 'error' ? '#e74c3c' : '#3498db',
269 height: 8, borderRadius: 4, transition: 'width 0.5s'
270 }} />
271 </div>
272 {wpProgress.status === 'success' && (
273 <div style={{ color: 'green', marginTop: 8 }}>
274 <b>Deployment completed!</b>
275 {wpProgress.siteUrl && (
276 <div style={{ marginTop: 12 }}>
277 <b>WordPress URL:</b> <a href={wpProgress.siteUrl} target="_blank" rel="noopener noreferrer">{wpProgress.siteUrl}</a>
278 </div>
279 )}
280 {wpProgress.credentials && (
281 <div style={{ marginTop: 12 }}>
282 <b>Credentials:</b>
283 <div style={{ marginTop: 4, fontFamily: 'monospace', fontSize: 14 }}>
284 Username: {wpProgress.credentials.username}<br />
285 Password: {wpProgress.credentials.password}
286 </div>
287 </div>
288 )}
289 </div>
290 )}
291 {wpProgress.status === 'error' && (
292 <div style={{ color: 'red', marginTop: 8 }}>
293 <b>Deployment failed.</b>
294 </div>
295 )}
296 </div>
297 </div>
298 )}
299 {/* ...existing code... */}
300 <div style={{ padding: '20px' }}>
301 <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '20px' }}>
302 <div>
303 <h2>Web Hosting & WordPress</h2>
304 <p style={{ color: 'var(--text-secondary)', marginTop: '8px' }}>
305 Manage your DigitalOcean App Platform apps and WordPress sites
306 </p>
307 </div>
308 <div style={{ display: 'flex', gap: '12px' }}>
309 <div style={{ position: 'relative' }} ref={dropdownRef}>
310 <button
311 onClick={() => setShowCreateDropdown(!showCreateDropdown)}
312 className="btn btn-primary"
313 style={{ display: 'flex', alignItems: 'center', gap: '8px' }}
314 >
315 <span className="material-symbols-outlined">add</span>
316 Create New
317 <span className="material-symbols-outlined" style={{ fontSize: '18px' }}>
318 {showCreateDropdown ? 'expand_less' : 'expand_more'}
319 </span>
320 </button>
321 {showCreateDropdown && (
322 <div style={{
323 position: 'absolute',
324 top: '100%',
325 right: 0,
326 marginTop: '8px',
327 background: 'var(--card-bg, #ffffff)',
328 backgroundColor: 'var(--card-bg, #ffffff)',
329 border: '1px solid var(--border)',
330 borderRadius: '8px',
331 boxShadow: '0 4px 12px rgba(0,0,0,0.15)',
332 minWidth: '200px',
333 zIndex: 1000,
334 opacity: 1
335 }}>
336 <button
337 onClick={() => handleCreateClick('wordpress')}
338 style={{
339 width: '100%',
340 padding: '12px 16px',
341 border: 'none',
342 background: 'none',
343 textAlign: 'left',
344 cursor: 'pointer',
345 display: 'flex',
346 alignItems: 'center',
347 gap: '12px',
348 borderBottom: '1px solid var(--border)'
349 }}
350 onMouseEnter={(e) => e.target.style.background = 'var(--hover-bg)'}
351 onMouseLeave={(e) => e.target.style.background = 'none'}
352 >
353 <span className="material-symbols-outlined" style={{ color: 'var(--primary)' }}>
354 web
355 </span>
356 <div>
357 <div style={{ fontWeight: '500' }}>WordPress Site</div>
358 <div style={{ fontSize: '12px', color: 'var(--text-secondary)' }}>
359 Full WordPress installation
360 </div>
361 </div>
362 </button>
363 <button
364 onClick={() => handleCreateClick('nodejs')}
365 style={{
366 width: '100%',
367 padding: '12px 16px',
368 border: 'none',
369 background: 'none',
370 textAlign: 'left',
371 cursor: 'pointer',
372 display: 'flex',
373 alignItems: 'center',
374 gap: '12px'
375 }}
376 onMouseEnter={(e) => e.target.style.background = 'var(--hover-bg)'}
377 onMouseLeave={(e) => e.target.style.background = 'none'}
378 >
379 <span className="material-symbols-outlined" style={{ color: 'var(--success)' }}>
380 code
381 </span>
382 <div>
383 <div style={{ fontWeight: '500' }}>Node.js App</div>
384 <div style={{ fontSize: '12px', color: 'var(--text-secondary)' }}>
385 Custom Node.js application
386 </div>
387 </div>
388 </button>
389 </div>
390 )}
391 </div>
392 <button
393 onClick={handleSync}
394 disabled={syncing}
395 className="btn btn-primary"
396 style={{ display: 'flex', alignItems: 'center', gap: '8px' }}
397 >
398 <span className="material-symbols-outlined" style={{ animation: syncing ? 'spin 1s linear infinite' : 'none' }}>
399 refresh
400 </span>
401 {syncing ? 'Syncing...' : 'Sync Now'}
402 </button>
403 {isRootUser && (
404 <>
405 <button
406 onClick={handleRenameAll}
407 disabled={renaming}
408 className="btn"
409 style={{ display: 'flex', alignItems: 'center', gap: '8px' }}
410 title="Add wordpress- or nodejs- prefix to all app names"
411 >
412 <span className="material-symbols-outlined">
413 edit
414 </span>
415 {renaming ? 'Renaming...' : 'Rename Apps'}
416 </button>
417 <button
418 onClick={handleOrganize}
419 disabled={organizing}
420 className="btn"
421 style={{ display: 'flex', alignItems: 'center', gap: '8px' }}
422 title="Organize apps into WordPress Projects and Node.js Projects folders"
423 >
424 <span className="material-symbols-outlined">
425 folder_managed
426 </span>
427 {organizing ? 'Organizing...' : 'Organize Apps'}
428 </button>
429 </>
430 )}
431 </div>
432 </div>
433
434 <div className="card" style={{ padding: '16px', marginBottom: '20px', background: 'var(--info-bg)', border: '1px solid var(--info-border)' }}>
435 <div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
436 <span className="material-symbols-outlined" style={{ color: 'var(--info-text)' }}>
437 schedule
438 </span>
439 <div style={{ fontSize: '14px', color: 'var(--info-text)' }}>
440 Data refreshes every 5 minutes from DigitalOcean.
441 {lastSync && (
442 <span style={{ marginLeft: '8px', opacity: 0.8 }}>
443 Last synced {formatTime(lastSync)}
444 </span>
445 )}
446 </div>
447 </div>
448 </div>
449
450 {error && (
451 <div className="card" style={{ padding: '16px', marginBottom: '20px', background: 'var(--error-bg)', border: '1px solid var(--error-border)' }}>
452 <div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
453 <span className="material-symbols-outlined" style={{ color: 'var(--error-text)' }}>
454 error
455 </span>
456 <span style={{ fontSize: '14px', color: 'var(--error-text)' }}>{error}</span>
457 </div>
458 </div>
459 )}
460
461 {/* WordPress Create Form */}
462 {showWordPressForm && (
463 <div className="card" style={{ padding: '24px', marginBottom: '20px' }}>
464 <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '20px' }}>
465 <h3 style={{ margin: 0 }}>Create New WordPress Site</h3>
466 <button
467 onClick={() => setShowWordPressForm(false)}
468 className="btn"
469 style={{ padding: '4px 8px' }}
470 >
471 <span className="material-symbols-outlined">close</span>
472 </button>
473 </div>
474
475 <div style={{
476 padding: '12px 16px',
477 background: 'var(--info-bg)',
478 border: '1px solid var(--info-border)',
479 borderRadius: '4px',
480 marginBottom: '20px',
481 fontSize: '14px',
482 color: 'var(--info-text)'
483 }}>
484 <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
485 <span className="material-symbols-outlined" style={{ fontSize: '18px' }}>info</span>
486 <span>Database and Spaces credentials are configured globally in Settings (Root Tenant only)</span>
487 </div>
488 </div>
489
490 <form onSubmit={handleWordPressSubmit}>
491 <div style={{ display: 'grid', gap: '16px', gridTemplateColumns: '1fr 1fr' }}>
492 <div>
493 <label style={{ display: 'block', marginBottom: '8px', fontWeight: '500' }}>
494 Site Name *
495 </label>
496 <input
497 type="text"
498 name="siteName"
499 value={wordpressFormData.siteName}
500 onChange={handleWordPressInputChange}
501 required
502 placeholder="my-awesome-site"
503 style={{ width: '100%', padding: '8px 12px', border: '1px solid var(--border)', borderRadius: '4px' }}
504 />
505 <small style={{ color: 'var(--text-secondary)', fontSize: '12px' }}>
506 Lowercase letters, numbers, and hyphens only
507 </small>
508 </div>
509
510 <div>
511 <label style={{ display: 'block', marginBottom: '8px', fontWeight: '500' }}>
512 Customer
513 </label>
514 <select
515 name="customerId"
516 value={wordpressFormData.customerId}
517 onChange={handleWordPressInputChange}
518 style={{ width: '100%', padding: '8px 12px', border: '1px solid var(--border)', borderRadius: '4px' }}
519 >
520 <option value="">Select customer (optional)</option>
521 {customers.map(customer => (
522 <option key={customer.customer_id} value={customer.customer_id}>
523 {customer.name}
524 </option>
525 ))}
526 </select>
527 </div>
528
529 <div>
530 <label style={{ display: 'block', marginBottom: '8px', fontWeight: '500' }}>
531 Domain (Optional)
532 </label>
533 <input
534 type="text"
535 name="domain"
536 value={wordpressFormData.domain}
537 onChange={handleWordPressInputChange}
538 placeholder="example.com"
539 style={{ width: '100%', padding: '8px 12px', border: '1px solid var(--border)', borderRadius: '4px' }}
540 />
541 <small style={{ color: 'var(--text-secondary)', fontSize: '12px' }}>
542 Leave empty to use DO App Platform domain
543 </small>
544 </div>
545 </div>
546
547 <div style={{ marginTop: '24px', display: 'flex', gap: '12px', justifyContent: 'flex-end' }}>
548 <button
549 type="button"
550 onClick={() => setShowWordPressForm(false)}
551 className="btn"
552 >
553 Cancel
554 </button>
555 <button
556 type="submit"
557 disabled={creatingWordPress}
558 className="btn btn-primary"
559 style={{ display: 'flex', alignItems: 'center', gap: '8px' }}
560 >
561 {creatingWordPress ? (
562 <>
563 <span className="spinner" style={{ width: '16px', height: '16px' }}></span>
564 Creating...
565 </>
566 ) : (
567 <>
568 <span className="material-symbols-outlined">rocket_launch</span>
569 Create WordPress Site
570 </>
571 )}
572 </button>
573 </div>
574 </form>
575 </div>
576 )}
577
578 {apps.length === 0 ? (
579 <div className="card" style={{ padding: '60px 20px', textAlign: 'center' }}>
580 <span className="material-symbols-outlined" style={{ fontSize: '64px', marginBottom: '16px', display: 'block', opacity: 0.5, color: 'var(--text-secondary)' }}>
581 public
582 </span>
583 <p style={{ color: 'var(--text-secondary)' }}>No apps found</p>
584 <p style={{ fontSize: '14px', color: 'var(--text-tertiary)', marginTop: '8px' }}>
585 Deploy an app to DigitalOcean App Platform to get started
586 </p>
587 </div>
588 ) : (
589 <div style={{ display: 'grid', gap: '20px', gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 1fr))' }}>
590 {apps.map((app) => (
591 <div key={app.app_id} className="card">
592 <div style={{ padding: '16px', borderBottom: '1px solid var(--border)' }}>
593 <div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between' }}>
594 <div>
595 <h3
596 style={{
597 fontSize: '18px',
598 marginBottom: '4px',
599 cursor: 'pointer',
600 color: 'var(--primary)',
601 textDecoration: 'none'
602 }}
603 onClick={() => navigate(`/hosting/apps/${app.app_id}`)}
604 onMouseOver={(e) => e.target.style.textDecoration = 'underline'}
605 onMouseOut={(e) => e.target.style.textDecoration = 'none'}
606 >
607 {app.app_name}
608 </h3>
609 <p style={{ fontSize: '14px', color: 'var(--text-secondary)' }}>
610 {app.customer_name || 'Unassigned'}
611 </p>
612 </div>
613 <span className={`badge badge-${getStatusColor(app.status)}`}>
614 {app.status}
615 </span>
616 </div>
617 </div>
618 <div style={{ padding: '16px' }}>
619 <dl style={{ display: 'grid', gap: '12px', fontSize: '14px' }}>
620 <div>
621 <dt style={{ color: 'var(--text-secondary)', marginBottom: '4px' }}>Region</dt>
622 <dd style={{ fontWeight: '500' }}>{app.region}</dd>
623 </div>
624 {app.default_ingress && (
625 <div>
626 <dt style={{ color: 'var(--text-secondary)', marginBottom: '4px' }}>URL</dt>
627 <dd style={{ fontWeight: '500' }}>
628 <a
629 href={`https://${app.default_ingress}`}
630 target="_blank"
631 rel="noopener noreferrer"
632 style={{ color: 'var(--primary)', textDecoration: 'none', wordBreak: 'break-all' }}
633 onMouseOver={(e) => e.target.style.textDecoration = 'underline'}
634 onMouseOut={(e) => e.target.style.textDecoration = 'none'}
635 >
636 {app.default_ingress}
637 </a>
638 </dd>
639 </div>
640 )}
641 {app.live_domain && (
642 <div>
643 <dt style={{ color: 'var(--text-secondary)', marginBottom: '4px' }}>Custom Domain</dt>
644 <dd style={{ fontWeight: '500' }}>
645 <a
646 href={`https://${app.live_domain}`}
647 target="_blank"
648 rel="noopener noreferrer"
649 style={{ color: 'var(--primary)', textDecoration: 'none', wordBreak: 'break-all' }}
650 onMouseOver={(e) => e.target.style.textDecoration = 'underline'}
651 onMouseOut={(e) => e.target.style.textDecoration = 'none'}
652 >
653 {app.live_domain}
654 </a>
655 </dd>
656 </div>
657 )}
658 {app.tier && (
659 <div>
660 <dt style={{ color: 'var(--text-secondary)', marginBottom: '4px' }}>Tier</dt>
661 <dd style={{ fontWeight: '500', textTransform: 'capitalize' }}>{app.tier}</dd>
662 </div>
663 )}
664 </dl>
665 </div>
666 </div>
667 ))}
668 </div>
669 )}
670
671 {/* WordPress Sites Section */}
672 {wordpressSites.length > 0 && (
673 <>
674 <div style={{ marginTop: '40px', marginBottom: '20px' }}>
675 <h3 style={{ fontSize: '20px', marginBottom: '8px' }}>WordPress Sites</h3>
676 <p style={{ color: 'var(--text-secondary)', fontSize: '14px' }}>
677 Managed WordPress installations
678 </p>
679 </div>
680 <div style={{ display: 'grid', gap: '20px', gridTemplateColumns: 'repeat(auto-fill, minmax(350px, 1fr))' }}>
681 {wordpressSites.map((site) => (
682 <div key={site.app_id || site.site_id || site.domain} className="card">
683 <div style={{ padding: '16px', borderBottom: '1px solid var(--border)' }}>
684 <div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', marginBottom: '12px' }}>
685 <div>
686 <div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '4px' }}>
687 <span className="material-symbols-outlined" style={{ fontSize: '20px', color: 'var(--primary)' }}>
688 web
689 </span>
690 <h3 style={{ margin: 0 }}>{site.name || site.domain}</h3>
691 </div>
692 {site.customer_name && (
693 <p style={{ fontSize: '13px', color: 'var(--text-secondary)', margin: 0 }}>
694 {site.customer_name}
695 </p>
696 )}
697 {site.is_self_hosted && (
698 <p style={{ fontSize: '12px', color: 'var(--text-secondary)', margin: '4px 0 0 0' }}>
699 Self-Hosted • {site.server_ip}
700 </p>
701 )}
702 </div>
703 <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-end', gap: '4px' }}>
704 <span className={`badge badge-${getStatusColor(site.status)}`}>
705 {site.status || 'unknown'}
706 </span>
707 {site.malware_issues > 0 && (
708 <span className="badge badge-danger" style={{ fontSize: '11px' }}>
709 {site.malware_issues} Security Issue{site.malware_issues !== 1 ? 's' : ''}
710 </span>
711 )}
712 </div>
713 </div>
714
715 {(site.url || site.domain) && (
716 <a
717 href={site.url || `https://${site.domain}`}
718 target="_blank"
719 rel="noopener noreferrer"
720 style={{
721 fontSize: '13px',
722 color: 'var(--primary)',
723 textDecoration: 'none',
724 display: 'flex',
725 alignItems: 'center',
726 gap: '4px'
727 }}
728 >
729 <span>{site.url || site.domain}</span>
730 <span className="material-symbols-outlined" style={{ fontSize: '16px' }}>
731 open_in_new
732 </span>
733 </a>
734 )}
735 </div>
736
737 <div style={{ padding: '16px' }}>
738 <div style={{ display: 'grid', gap: '8px', fontSize: '13px' }}>
739 {site.is_self_hosted ? (
740 <>
741 <div style={{ display: 'flex', justifyContent: 'space-between' }}>
742 <span style={{ color: 'var(--text-secondary)' }}>WordPress:</span>
743 <span>{site.wp_version?.trim() || 'Unknown'}</span>
744 </div>
745 <div style={{ display: 'flex', justifyContent: 'space-between' }}>
746 <span style={{ color: 'var(--text-secondary)' }}>Plugins:</span>
747 <span>{site.plugin_count || 0}</span>
748 </div>
749 <div style={{ display: 'flex', justifyContent: 'space-between' }}>
750 <span style={{ color: 'var(--text-secondary)' }}>Disk Usage:</span>
751 <span>{site.disk_usage || 'N/A'}</span>
752 </div>
753 {site.last_checked && (
754 <div style={{ display: 'flex', justifyContent: 'space-between' }}>
755 <span style={{ color: 'var(--text-secondary)' }}>Last Checked:</span>
756 <span style={{ fontSize: '12px' }}>
757 {new Date(site.last_checked).toLocaleString()}
758 </span>
759 </div>
760 )}
761 </>
762 ) : (
763 <>
764 <div style={{ display: 'flex', justifyContent: 'space-between' }}>
765 <span style={{ color: 'var(--text-secondary)' }}>Region:</span>
766 <span>{site.region || 'syd1'}</span>
767 </div>
768 <div style={{ display: 'flex', justifyContent: 'space-between' }}>
769 <span style={{ color: 'var(--text-secondary)' }}>Created:</span>
770 <span>{new Date(site.created_at).toLocaleDateString()}</span>
771 </div>
772 {site.app_id && (
773 <div style={{ display: 'flex', justifyContent: 'space-between' }}>
774 <span style={{ color: 'var(--text-secondary)' }}>App ID:</span>
775 <span style={{ fontFamily: 'monospace', fontSize: '11px' }}>
776 {site.app_id.substring(0, 12)}...
777 </span>
778 </div>
779 )}
780 </>
781 )}
782 </div>
783
784 <div style={{ marginTop: '16px', display: 'flex', gap: '8px' }}>
785 {site.is_self_hosted ? (
786 <>
787 <button
788 className="btn btn-sm btn-primary"
789 onClick={() => window.open(`https://${site.domain}/wp-admin`, '_blank')}
790 style={{ flex: 1, fontSize: '12px' }}
791 >
792 <span className="material-symbols-outlined" style={{ fontSize: '16px' }}>
793 login
794 </span>
795 WordPress Login
796 </button>
797 {site.malware_issues > 0 && (
798 <button
799 className="btn btn-sm"
800 onClick={() => alert(`Security Issues:\n- ${site.malware_issues} suspicious files detected\n\nCheck server logs at:\n/var/log/wordpress-security-alerts.log`)}
801 style={{ fontSize: '12px', color: 'var(--danger)' }}
802 >
803 <span className="material-symbols-outlined" style={{ fontSize: '16px' }}>
804 warning
805 </span>
806 </button>
807 )}
808 </>
809 ) : (
810 <>
811 <button
812 className="btn btn-sm"
813 onClick={() => window.open(`https://cloud.digitalocean.com/apps/${site.app_id}`, '_blank')}
814 disabled={!site.app_id}
815 style={{ flex: 1, fontSize: '12px' }}
816 >
817 <span className="material-symbols-outlined" style={{ fontSize: '16px' }}>
818 settings
819 </span>
820 Manage
821 </button>
822 </>
823 )}
824 <button
825 className="btn btn-sm"
826 onClick={() => fetchWordPressSites()}
827 style={{ fontSize: '12px' }}
828 >
829 <span className="material-symbols-outlined" style={{ fontSize: '16px' }}>
830 refresh
831 </span>
832 </button>
833 </div>
834 </div>
835 </div>
836 ))}
837 </div>
838 </>
839 )}
840
841 {/* Toast Notifications */}
842 {toasts.map(toast => (
843 <Toast
844 key={toast.id}
845 message={toast.message}
846 type={toast.type}
847 duration={toast.duration}
848 onClose={() => hideToast(toast.id)}
849 />
850 ))}
851 </div>
852 </>
853 );
854}