feat: ordenar backups por cliente, generar codigos unicos de cliente UUID y soporte para compilar agente desde la web
This commit is contained in:
@@ -10,7 +10,8 @@ import {
|
||||
Server as ServerIcon,
|
||||
X,
|
||||
Edit2,
|
||||
RefreshCw
|
||||
RefreshCw,
|
||||
Cpu
|
||||
} from 'lucide-react';
|
||||
import { ClientItem, api } from '../services/api';
|
||||
|
||||
@@ -30,6 +31,65 @@ export const ClientsView: React.FC<ClientsViewProps> = ({ clients, onRefresh })
|
||||
const [isCopied, setIsCopied] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// Agent Compilation states
|
||||
const [showBuildModal, setShowBuildModal] = useState(false);
|
||||
const [buildStatus, setBuildStatus] = useState<{
|
||||
status: string;
|
||||
started_at: string | null;
|
||||
finished_at: string | null;
|
||||
error_message: string | null;
|
||||
}>({ status: 'IDLE', started_at: null, finished_at: null, error_message: null });
|
||||
const [buildLoading, setBuildLoading] = useState(false);
|
||||
|
||||
const pollBuildStatus = async () => {
|
||||
try {
|
||||
const statusData = await api.getAgentBuildStatus();
|
||||
setBuildStatus(statusData);
|
||||
return statusData.status;
|
||||
} catch (err) {
|
||||
console.error("Error polling build status:", err);
|
||||
return 'FAILED';
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
let intervalId: any;
|
||||
if (showBuildModal) {
|
||||
pollBuildStatus();
|
||||
intervalId = setInterval(async () => {
|
||||
const statusStr = await pollBuildStatus();
|
||||
if (statusStr !== 'BUILDING') {
|
||||
clearInterval(intervalId);
|
||||
}
|
||||
}, 2000);
|
||||
}
|
||||
return () => {
|
||||
if (intervalId) clearInterval(intervalId);
|
||||
};
|
||||
}, [showBuildModal]);
|
||||
|
||||
const handleStartBuild = async () => {
|
||||
setBuildLoading(true);
|
||||
try {
|
||||
await api.buildAgent();
|
||||
setBuildStatus(prev => ({ ...prev, status: 'BUILDING' }));
|
||||
// Trigger status fetch
|
||||
pollBuildStatus();
|
||||
} catch (err: any) {
|
||||
alert(`Error iniciando compilación: ${err.message}`);
|
||||
} finally {
|
||||
setBuildLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDownloadAgent = async () => {
|
||||
try {
|
||||
await api.downloadAgent();
|
||||
} catch (err: any) {
|
||||
alert(`Error descargando el agente: ${err.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
const formatBytes = (bytes: number) => {
|
||||
if (bytes === 0) return '0 B';
|
||||
const k = 1024;
|
||||
@@ -134,10 +194,16 @@ export const ClientsView: React.FC<ClientsViewProps> = ({ clients, onRefresh })
|
||||
Administración centralizada de agentes Windows 10, 11 y Windows Server
|
||||
</p>
|
||||
</div>
|
||||
<button className="btn btn-primary" onClick={() => { setShowModal(true); setGeneratedCode(null); }}>
|
||||
<Plus size={16} />
|
||||
Registrar Nuevo Cliente
|
||||
</button>
|
||||
<div style={{ display: 'flex', gap: '12px' }}>
|
||||
<button className="btn btn-secondary" onClick={() => setShowBuildModal(true)}>
|
||||
<Cpu size={16} style={{ marginRight: '6px' }} />
|
||||
Generar Agente (.exe)
|
||||
</button>
|
||||
<button className="btn btn-primary" onClick={() => { setShowModal(true); setGeneratedCode(null); }}>
|
||||
<Plus size={16} />
|
||||
Registrar Nuevo Cliente
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="glass-card">
|
||||
@@ -415,6 +481,98 @@ export const ClientsView: React.FC<ClientsViewProps> = ({ clients, onRefresh })
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showBuildModal && (
|
||||
<div className="modal-backdrop">
|
||||
<div className="modal-content" style={{ maxWidth: '560px' }}>
|
||||
<div className="modal-header">
|
||||
<h4 style={{ fontSize: '1.1rem', fontWeight: 700, display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
<Cpu size={18} color="var(--accent-cyan)" />
|
||||
Generación del Cliente de Distribución
|
||||
</h4>
|
||||
<button className="modal-close" onClick={() => setShowBuildModal(false)}>
|
||||
<X size={18} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="modal-body" style={{ padding: '16px 0', display: 'flex', flexDirection: 'column', gap: '16px' }}>
|
||||
<p style={{ fontSize: '0.84rem', color: 'var(--text-muted)', lineHeight: '1.5' }}>
|
||||
Compila de forma centralizada y empaqueta el agente de Windows como un ejecutable independiente (.EXE)
|
||||
listo para ser distribuido e instalado en clientes fuera de la red local (Internet).
|
||||
</p>
|
||||
|
||||
<div className="glass-card" style={{ padding: '16px', display: 'flex', flexDirection: 'column', gap: '12px' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<span style={{ fontSize: '0.86rem', fontWeight: 600 }}>Estado del Compilador:</span>
|
||||
<span className={`badge ${
|
||||
buildStatus.status === 'IDLE' ? '' :
|
||||
buildStatus.status === 'BUILDING' ? 'badge-syncing' :
|
||||
buildStatus.status === 'SUCCESS' ? 'badge-online' : 'badge-offline'
|
||||
}`} style={{ textTransform: 'uppercase' }}>
|
||||
{buildStatus.status === 'IDLE' ? 'Listo / Inactivo' :
|
||||
buildStatus.status === 'BUILDING' ? 'Compilando...' :
|
||||
buildStatus.status === 'SUCCESS' ? 'Compilado con éxito' : 'Fallido'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{buildStatus.started_at && (
|
||||
<div style={{ fontSize: '0.76rem', color: 'var(--text-dim)' }}>
|
||||
Comenzó: {new Date(buildStatus.started_at).toLocaleString()}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{buildStatus.finished_at && (
|
||||
<div style={{ fontSize: '0.76rem', color: 'var(--text-dim)' }}>
|
||||
Finalizó: {new Date(buildStatus.finished_at).toLocaleString()}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{buildStatus.error_message && (
|
||||
<div style={{
|
||||
maxHeight: '150px',
|
||||
overflowY: 'auto',
|
||||
backgroundColor: 'rgba(239, 68, 68, 0.08)',
|
||||
border: '1px solid rgba(239, 68, 68, 0.2)',
|
||||
borderRadius: '6px',
|
||||
padding: '10px',
|
||||
fontFamily: 'var(--font-mono)',
|
||||
fontSize: '0.74rem',
|
||||
color: '#fca5a5',
|
||||
whiteSpace: 'pre-wrap'
|
||||
}}>
|
||||
{buildStatus.error_message}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="modal-footer" style={{ borderTop: '1px solid rgba(255,255,255,0.08)', paddingTop: '16px', display: 'flex', justifyContent: 'flex-end', gap: '12px' }}>
|
||||
<button className="btn btn-secondary" onClick={() => setShowBuildModal(false)}>
|
||||
Cerrar
|
||||
</button>
|
||||
|
||||
{buildStatus.status === 'SUCCESS' && (
|
||||
<button className="btn btn-primary" onClick={handleDownloadAgent}>
|
||||
Descargar Instalador (.exe)
|
||||
</button>
|
||||
)}
|
||||
|
||||
{buildStatus.status !== 'BUILDING' && (
|
||||
<button className="btn btn-primary" onClick={handleStartBuild} disabled={buildLoading}>
|
||||
{buildLoading ? 'Iniciando...' : buildStatus.status === 'SUCCESS' ? 'Volver a Compilar' : 'Compilar Agente'}
|
||||
</button>
|
||||
)}
|
||||
|
||||
{buildStatus.status === 'BUILDING' && (
|
||||
<button className="btn btn-primary" disabled style={{ opacity: 0.7 }}>
|
||||
<RefreshCw className="animate-spin" size={14} style={{ marginRight: '6px' }} />
|
||||
Compilando con PyInstaller...
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -191,7 +191,13 @@ export const RestoreView: React.FC<RestoreViewProps> = ({ clients }) => {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{filteredBackups.map((backup) => (
|
||||
{[...filteredBackups].sort((a, b) => {
|
||||
const clientA = getClientName(a.client_id).toLowerCase();
|
||||
const clientB = getClientName(b.client_id).toLowerCase();
|
||||
if (clientA < clientB) return -1;
|
||||
if (clientA > clientB) return 1;
|
||||
return new Date(b.created_at).getTime() - new Date(a.created_at).getTime();
|
||||
}).map((backup) => (
|
||||
<tr key={backup.id}>
|
||||
<td>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
|
||||
@@ -205,6 +205,29 @@ export const api = {
|
||||
request<{ code: string; expires_at: string; client_id: number }>(`/clients/${clientId}/re-register`, { method: 'POST' }),
|
||||
deleteClient: (clientId: number) =>
|
||||
request<{ message: string }>(`/clients/${clientId}`, { method: 'DELETE' }),
|
||||
buildAgent: () =>
|
||||
request<{ message: string }>('/clients/build-agent', { method: 'POST' }),
|
||||
getAgentBuildStatus: () =>
|
||||
request<{ status: string; started_at: string | null; finished_at: string | null; error_message: string | null }>('/clients/build-agent/status'),
|
||||
downloadAgent: async () => {
|
||||
const token = getAuthToken();
|
||||
const response = await fetch(`${API_BASE}/clients/download-agent`, {
|
||||
headers: token ? { 'Authorization': `Bearer ${token}` } : {},
|
||||
});
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({ detail: 'Network error' }));
|
||||
throw new Error(errorData.detail || `Download failed with status ${response.status}`);
|
||||
}
|
||||
const blob = await response.blob();
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'OnEverDriveAgent-Standalone.exe';
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
document.body.removeChild(a);
|
||||
},
|
||||
updateClient: (clientId: number, data: { alias?: string; storage_quota_bytes?: number }) =>
|
||||
request<ClientItem>(`/clients/${clientId}`, {
|
||||
method: 'PATCH',
|
||||
|
||||
Reference in New Issue
Block a user