feat: Initial commit for OnEver Drive centralized backup system with Windows PyQt6 agent and Proxmox LXC deployment

This commit is contained in:
2026-08-13 19:33:44 -03:00
commit 1bfb808c79
77 changed files with 10675 additions and 0 deletions
+288
View File
@@ -0,0 +1,288 @@
import React, { useState } from 'react';
import {
Plus,
HardDrive,
Copy,
Check,
ShieldAlert,
Trash2,
Laptop,
Server as ServerIcon,
X
} from 'lucide-react';
import { ClientItem, api } from '../services/api';
interface ClientsViewProps {
clients: ClientItem[];
onRefresh: () => void;
}
export const ClientsView: React.FC<ClientsViewProps> = ({ clients, onRefresh }) => {
const [showModal, setShowModal] = useState(false);
const [clientHint, setClientHint] = useState('');
const [generatedCode, setGeneratedCode] = useState<{ code: string; expires_at: string } | null>(null);
const [isCopied, setIsCopied] = useState(false);
const [loading, setLoading] = useState(false);
const formatBytes = (bytes: number) => {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
};
const handleGenerateCode = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
try {
const res = await api.createRegistrationCode(clientHint);
setGeneratedCode(res);
} catch (err: any) {
alert(`Error generating registration code: ${err.message}`);
} finally {
setLoading(false);
}
};
const handleRevoke = async (client: ClientItem) => {
if (confirm(`¿Estás seguro de revocar las credenciales para el cliente ${client.name} (${client.client_code})?`)) {
try {
await api.revokeClient(client.id);
onRefresh();
} catch (err: any) {
alert(`Error: ${err.message}`);
}
}
};
const handleDelete = async (client: ClientItem) => {
if (confirm(`¿Eliminar definitivamente el cliente ${client.name} y todos sus registros?`)) {
try {
await api.deleteClient(client.id);
onRefresh();
} catch (err: any) {
alert(`Error: ${err.message}`);
}
}
};
const serverUrl = window.location.origin;
const psCommand = generatedCode
? `python agent_cli.py register --server "${serverUrl}" --code "${generatedCode.code}"`
: '';
const copyToClipboard = (text: string) => {
navigator.clipboard.writeText(text);
setIsCopied(true);
setTimeout(() => setIsCopied(false), 2000);
};
return (
<div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '24px' }}>
<div>
<h3 style={{ fontSize: '1.2rem', fontWeight: 700 }}>Clientes Windows Registrados</h3>
<p style={{ fontSize: '0.84rem', color: 'var(--text-muted)', marginTop: '2px' }}>
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>
<div className="glass-card">
<div className="table-container">
<table className="modern-table">
<thead>
<tr>
<th>Cliente ID</th>
<th>Nombre / Hostname</th>
<th>Sistema Operativo</th>
<th>Dirección IP</th>
<th>Estado</th>
<th>Espacio Utilizado</th>
<th>Última Conexión</th>
<th style={{ textAlign: 'right' }}>Acciones</th>
</tr>
</thead>
<tbody>
{clients.map((client) => {
const isOnline = client.status === 'ONLINE';
return (
<tr key={client.id}>
<td>
<span className="hash-badge" style={{ color: 'var(--accent-cyan)' }}>
{client.client_code}
</span>
</td>
<td>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
{client.os_info?.includes('Server') ? (
<ServerIcon size={16} color="var(--accent-indigo)" />
) : (
<Laptop size={16} color="var(--text-muted)" />
)}
<div>
<div style={{ fontWeight: 600 }}>{client.name}</div>
<div style={{ fontSize: '0.74rem', color: 'var(--text-dim)' }}>
{client.hostname || 'Desconocido'} v{client.agent_version}
</div>
</div>
</div>
</td>
<td style={{ fontSize: '0.84rem', color: 'var(--text-muted)' }}>
{client.os_info || 'Windows'}
</td>
<td style={{ fontSize: '0.84rem', fontFamily: 'var(--font-mono)' }}>
{client.ip_address || '—'}
</td>
<td>
<span className={`badge ${isOnline ? 'badge-online' : 'badge-offline'}`}>
{client.status}
</span>
</td>
<td style={{ fontFamily: 'var(--font-mono)', fontSize: '0.84rem' }}>
{formatBytes(client.storage_used_bytes)}
</td>
<td style={{ fontSize: '0.82rem', color: 'var(--text-dim)' }}>
{client.last_seen_at
? new Date(client.last_seen_at).toLocaleString()
: 'Nunca'}
</td>
<td style={{ textAlign: 'right' }}>
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: '8px' }}>
<button
className="btn btn-secondary"
style={{ padding: '6px 10px', fontSize: '0.78rem' }}
onClick={() => handleRevoke(client)}
title="Revocar credenciales"
>
<ShieldAlert size={14} color="var(--accent-amber)" />
Revocar
</button>
<button
className="btn btn-danger"
style={{ padding: '6px 10px', fontSize: '0.78rem' }}
onClick={() => handleDelete(client)}
title="Eliminar cliente"
>
<Trash2 size={14} />
</button>
</div>
</td>
</tr>
);
})}
{clients.length === 0 && (
<tr>
<td colSpan={8} style={{ textAlign: 'center', padding: '40px', color: 'var(--text-dim)' }}>
No hay clientes Windows registrados. Haz clic en "Registrar Nuevo Cliente" para comenzar.
</td>
</tr>
)}
</tbody>
</table>
</div>
</div>
{/* Registration Modal */}
{showModal && (
<div className="modal-backdrop">
<div className="modal-card">
<div className="modal-header">
<h3 style={{ fontSize: '1.1rem', fontWeight: 700 }}>Registrar Agente Windows</h3>
<button
style={{ background: 'transparent', border: 'none', color: 'var(--text-muted)', cursor: 'pointer' }}
onClick={() => setShowModal(false)}
>
<X size={20} />
</button>
</div>
{!generatedCode ? (
<form onSubmit={handleGenerateCode}>
<div className="form-group">
<label>Nombre identificador del equipo (Opcional):</label>
<input
type="text"
className="form-input"
placeholder="Ej: Servidor SQL Producción"
value={clientHint}
onChange={(e) => setClientHint(e.target.value)}
/>
<p style={{ fontSize: '0.78rem', color: 'var(--text-dim)', marginTop: '4px' }}>
Se generará un código de un solo uso válido por 48 horas.
</p>
</div>
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: '12px', marginTop: '24px' }}>
<button type="button" className="btn btn-secondary" onClick={() => setShowModal(false)}>
Cancelar
</button>
<button type="submit" className="btn btn-primary" disabled={loading}>
{loading ? 'Generando...' : 'Generar Código de Registro'}
</button>
</div>
</form>
) : (
<div>
<div style={{ textAlign: 'center', margin: '16px 0 24px 0' }}>
<div style={{ fontSize: '0.8rem', color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: '0.05em' }}>
Código de Registro Único
</div>
<div
style={{
fontSize: '2rem',
fontWeight: 800,
color: 'var(--accent-cyan)',
fontFamily: 'var(--font-mono)',
letterSpacing: '0.1em',
marginTop: '6px',
}}
>
{generatedCode.code}
</div>
<div style={{ fontSize: '0.75rem', color: 'var(--text-dim)', marginTop: '4px' }}>
Expira: {new Date(generatedCode.expires_at).toLocaleString()}
</div>
</div>
<div className="form-group">
<label>Comando de instalación en Windows (PowerShell / CMD):</label>
<div className="code-box">
<span>{psCommand}</span>
<button
type="button"
className="btn btn-secondary"
style={{ padding: '6px 10px', fontSize: '0.75rem' }}
onClick={() => copyToClipboard(psCommand)}
>
{isCopied ? <Check size={14} color="var(--accent-emerald)" /> : <Copy size={14} />}
{isCopied ? 'Copiado' : 'Copiar'}
</button>
</div>
</div>
<div style={{ display: 'flex', justifyContent: 'flex-end', marginTop: '24px' }}>
<button
type="button"
className="btn btn-primary"
onClick={() => {
setShowModal(false);
onRefresh();
}}
>
Listo
</button>
</div>
</div>
)}
</div>
</div>
)}
</div>
);
};