1import { useState, useEffect } from 'react';
2import { useNavigate } from 'react-router-dom';
3import { apiFetch } from '../../lib/api';
5export default function TabServers() {
6 const navigate = useNavigate();
7 const [droplets, setDroplets] = useState([]);
8 const [metrics, setMetrics] = useState({});
9 const [loading, setLoading] = useState(true);
10 const [error, setError] = useState('');
11 const [syncing, setSyncing] = useState(false);
12 const [lastSync, setLastSync] = useState(null);
18 const fetchDroplets = async () => {
22 const res = await apiFetch('/hosting/droplets');
24 const data = await res.json();
26 // Filter out WordPress and Node.js droplets
27 const filteredDroplets = (data.droplets || []).filter(droplet => {
28 const name = droplet.droplet_name?.toLowerCase() || '';
29 const tags = droplet.tags || [];
30 const tagString = tags.join(' ').toLowerCase();
32 // Exclude if name or tags contain wordpress, node, or nodejs
33 return !name.includes('wordpress') &&
34 !name.includes('node') &&
35 !name.includes('nodejs') &&
36 !tagString.includes('wordpress') &&
37 !tagString.includes('node');
40 setDroplets(filteredDroplets);
41 setLastSync(data.lastSync);
43 // Metrics are already cached in the droplet records
44 const metricsData = {};
45 for (const droplet of filteredDroplets) {
46 metricsData[droplet.do_droplet_id] = {
47 cpu_usage_percent: droplet.cpu_usage_percent,
48 memory_usage_percent: droplet.memory_usage_percent,
49 disk_usage_percent: droplet.disk_usage_percent
52 setMetrics(metricsData);
54 const errorData = await res.json();
55 setError(errorData.error || 'Failed to fetch droplets');
58 console.error('Error fetching droplets:', err);
59 setError(err.message || 'Failed to load droplets');
65 const handleSync = async () => {
69 const res = await apiFetch('/hosting/sync', { method: 'POST' });
71 await fetchDroplets();
73 const errorData = await res.json();
74 setError(errorData.error || 'Failed to sync');
77 console.error('Error syncing droplets:', err);
78 setError(err.message || 'Failed to sync droplets');
84 const getStatusColor = (status) => {
93 return statusMap[status?.toLowerCase()] || 'secondary';
96 const getStatusLabel = (status) => {
97 if (status?.toLowerCase() === 'deleted') return 'terminated';
101 const formatTime = (date) => {
102 if (!date) return '';
103 const now = new Date();
104 const then = new Date(date);
105 const diff = Math.floor((now - then) / 1000);
107 if (diff < 60) return `${diff} seconds ago`;
108 if (diff < 3600) return `${Math.floor(diff / 60)} minutes ago`;
109 if (diff < 86400) return `${Math.floor(diff / 3600)} hours ago`;
110 return `${Math.floor(diff / 86400)} days ago`;
115 <div style={{ padding: '40px', textAlign: 'center' }}>
116 <div className="spinner"></div>
122 <div style={{ padding: '20px' }}>
123 <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '20px' }}>
125 <h2>Servers (Droplets)</h2>
126 <p style={{ color: 'var(--text-secondary)', marginTop: '8px' }}>
127 Manage your DigitalOcean droplets
133 className="btn btn-primary"
134 style={{ display: 'flex', alignItems: 'center', gap: '8px' }}
136 <span className="material-symbols-outlined" style={{ animation: syncing ? 'spin 1s linear infinite' : 'none' }}>
139 {syncing ? 'Syncing...' : 'Sync Now'}
143 <div className="card" style={{ padding: '16px', marginBottom: '20px', background: 'var(--info-bg)', border: '1px solid var(--info-border)' }}>
144 <div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
145 <span className="material-symbols-outlined" style={{ color: 'var(--info-text)' }}>
148 <div style={{ fontSize: '14px', color: 'var(--info-text)' }}>
149 Data refreshes every 5 minutes from DigitalOcean.
151 <span style={{ marginLeft: '8px', opacity: 0.8 }}>
152 Last synced {formatTime(lastSync)}
160 <div className="card" style={{ padding: '16px', marginBottom: '20px', background: 'var(--error-bg)', border: '1px solid var(--error-border)' }}>
161 <div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
162 <span className="material-symbols-outlined" style={{ color: 'var(--error-text)' }}>
165 <span style={{ fontSize: '14px', color: 'var(--error-text)' }}>{error}</span>
170 {droplets.length === 0 ? (
171 <div className="card" style={{ padding: '60px 20px', textAlign: 'center' }}>
172 <span className="material-symbols-outlined" style={{ fontSize: '64px', marginBottom: '16px', display: 'block', opacity: 0.5, color: 'var(--text-secondary)' }}>
175 <p style={{ color: 'var(--text-secondary)' }}>No droplets found</p>
176 <p style={{ fontSize: '14px', color: 'var(--text-tertiary)', marginTop: '8px' }}>
177 Create a droplet in your DigitalOcean account
181 <div style={{ display: 'grid', gap: '20px', gridTemplateColumns: 'repeat(auto-fill, minmax(350px, 1fr))' }}>
182 {droplets.map((droplet) => {
183 const dropletMetrics = metrics[droplet.do_droplet_id];
184 const isActive = droplet.status?.toLowerCase() === 'active';
186 // Calculate average CPU if metrics available
188 if (dropletMetrics && dropletMetrics.data && dropletMetrics.data.result && dropletMetrics.data.result.length > 0) {
189 const values = dropletMetrics.data.result[0].values || [];
190 if (values.length > 0) {
191 const sum = values.reduce((acc, [, value]) => acc + parseFloat(value), 0);
192 avgCpu = (sum / values.length).toFixed(1);
198 key={droplet.droplet_id}
200 style={{ overflow: 'hidden', cursor: 'pointer' }}
201 onClick={() => navigate(`/hosting/servers/${droplet.droplet_id}`)}
203 <div style={{ padding: '20px', borderBottom: '1px solid var(--border)' }}>
204 <div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', marginBottom: '12px' }}>
205 <div style={{ flex: 1 }}>
206 <h3 style={{ fontSize: '18px', marginBottom: '6px', fontWeight: '600' }}>{droplet.droplet_name}</h3>
207 <p style={{ fontSize: '13px', color: 'var(--text-secondary)' }}>
208 {droplet.customer_name || 'Unassigned'}
211 <span className={`badge badge-${getStatusColor(droplet.status)}`} style={{ textTransform: 'capitalize' }}>
212 {isActive && <span style={{ marginRight: '4px' }}>●</span>}
213 {getStatusLabel(droplet.status)}
217 {droplet.ip_address && (
218 <div style={{ fontSize: '13px', fontFamily: 'monospace', color: 'var(--text-secondary)' }}>
224 {/* Metrics Section */}
226 <div style={{ padding: '16px', background: 'var(--surface-2, #f8f9fa)', borderBottom: '1px solid var(--border)' }}>
227 <div style={{ display: 'flex', alignItems: 'center', gap: '12px', marginBottom: '8px' }}>
228 <span style={{ fontSize: '13px', color: 'var(--text-secondary)', fontWeight: '500' }}>CPU Usage (avg)</span>
229 <span style={{ fontSize: '16px', fontWeight: '600', color: 'var(--text-primary)', marginLeft: 'auto' }}>
233 <div style={{ width: '100%', height: '6px', background: 'var(--border)', borderRadius: '3px', overflow: 'hidden' }}>
235 width: `${Math.min(avgCpu, 100)}%`,
237 background: avgCpu > 80 ? 'var(--error)' : avgCpu > 50 ? 'var(--warning)' : 'var(--success)',
238 transition: 'width 0.3s ease'
244 <div style={{ padding: '20px' }}>
245 <dl style={{ display: 'grid', gap: '12px', fontSize: '14px', margin: 0 }}>
246 <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
247 <dt style={{ color: 'var(--text-secondary)', fontWeight: '500' }}>Region</dt>
248 <dd style={{ margin: 0, fontWeight: '600', color: 'var(--text-primary)' }}>{droplet.region}</dd>
250 <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
251 <dt style={{ color: 'var(--text-secondary)', fontWeight: '500' }}>Size</dt>
252 <dd style={{ margin: 0, fontWeight: '600', color: 'var(--text-primary)' }}>{droplet.size}</dd>
255 <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
256 <dt style={{ color: 'var(--text-secondary)', fontWeight: '500' }}>Resources</dt>
257 <dd style={{ margin: 0, fontWeight: '600', color: 'var(--text-primary)' }}>
258 {droplet.vcpus} vCPU • {(droplet.memory / 1024).toFixed(0)} GB • {droplet.disk} GB
264 <dt style={{ color: 'var(--text-secondary)', marginBottom: '4px' }}>Image</dt>
265 <dd style={{ margin: 0, fontWeight: '500', fontSize: '12px', background: 'var(--surface-1, #fff)', padding: '8px', borderRadius: '4px', border: '1px solid var(--border)' }}>
270 {droplet.tags && droplet.tags.length > 0 && (
272 <dt style={{ color: 'var(--text-secondary)', marginBottom: '4px' }}>Tags</dt>
273 <dd style={{ display: 'flex', flexWrap: 'wrap', gap: '4px', marginTop: '4px', margin: 0 }}>
274 {droplet.tags.map((tag, i) => (
275 <span key={i} className="badge badge-secondary" style={{ fontSize: '11px' }}>