feat: ordenar backups por cliente, generar codigos unicos de cliente UUID y soporte para compilar agente desde la web

This commit is contained in:
Carlos Tello
2026-08-17 00:00:03 -03:00
parent bdce625767
commit 7badc34816
4 changed files with 280 additions and 11 deletions
+163 -5
View File
@@ -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>
);
};
+7 -1
View File
@@ -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' }}>
+23
View File
@@ -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',