feat: implementar Fases 3, 4 y 5 del roadmap (Planificacion Avanzada, Historial de Corridas y Copia Local Duplicada)
This commit is contained in:
@@ -37,9 +37,30 @@ export const JobsView: React.FC<JobsViewProps> = ({ jobs, clients, onRefresh, se
|
||||
skip_system: false,
|
||||
skip_readonly: false,
|
||||
sync_deletions: false,
|
||||
local_destination_path: '',
|
||||
});
|
||||
const [editingJobId, setEditingJobId] = useState<number | null>(null);
|
||||
|
||||
const [showRunsModal, setShowRunsModal] = useState(false);
|
||||
const [selectedJobForRuns, setSelectedJobForRuns] = useState<BackupJobItem | null>(null);
|
||||
const [runsHistory, setRunsHistory] = useState<JobRunItem[]>([]);
|
||||
const [loadingRuns, setLoadingRuns] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const handleShowRuns = async (job: BackupJobItem) => {
|
||||
setSelectedJobForRuns(job);
|
||||
setShowRunsModal(true);
|
||||
setLoadingRuns(true);
|
||||
try {
|
||||
const history = await api.getJobRuns(job.id);
|
||||
setRunsHistory(history);
|
||||
} catch (err: any) {
|
||||
alert(`Error obteniendo historial: ${err.message}`);
|
||||
} finally {
|
||||
setLoadingRuns(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmitJob = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!formData.client_id) {
|
||||
@@ -79,6 +100,7 @@ export const JobsView: React.FC<JobsViewProps> = ({ jobs, clients, onRefresh, se
|
||||
skip_system: job.skip_system,
|
||||
skip_readonly: job.skip_readonly,
|
||||
sync_deletions: job.sync_deletions,
|
||||
local_destination_path: job.local_destination_path || '',
|
||||
});
|
||||
setShowModal(true);
|
||||
};
|
||||
@@ -141,6 +163,7 @@ export const JobsView: React.FC<JobsViewProps> = ({ jobs, clients, onRefresh, se
|
||||
skip_system: false,
|
||||
skip_readonly: false,
|
||||
sync_deletions: false,
|
||||
local_destination_path: '',
|
||||
});
|
||||
setShowModal(true);
|
||||
}}
|
||||
@@ -186,9 +209,17 @@ export const JobsView: React.FC<JobsViewProps> = ({ jobs, clients, onRefresh, se
|
||||
{getClientName(job.client_id)}
|
||||
</td>
|
||||
<td>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '6px', fontSize: '0.82rem', fontFamily: 'var(--font-mono)' }}>
|
||||
<Folder size={14} color="var(--accent-cyan)" />
|
||||
{job.source_path}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '6px', fontSize: '0.82rem', fontFamily: 'var(--font-mono)' }}>
|
||||
<Folder size={14} color="var(--accent-cyan)" />
|
||||
{job.source_path}
|
||||
</div>
|
||||
{job.local_destination_path && (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '6px', fontSize: '0.74rem', fontFamily: 'var(--font-mono)', color: 'var(--text-muted)' }}>
|
||||
<span style={{ color: '#fbbf24', fontWeight: 600 }}>Copia Local:</span>
|
||||
{job.local_destination_path}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
@@ -223,6 +254,14 @@ export const JobsView: React.FC<JobsViewProps> = ({ jobs, clients, onRefresh, se
|
||||
<Play size={13} />
|
||||
Ejecutar
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-secondary"
|
||||
style={{ padding: '6px 10px', fontSize: '0.78rem', color: '#fbbf24' }}
|
||||
onClick={() => handleShowRuns(job)}
|
||||
title="Ver historial de ejecuciones"
|
||||
>
|
||||
<Clock size={13} />
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-secondary"
|
||||
style={{ padding: '6px 10px', fontSize: '0.78rem', color: 'var(--text-muted)' }}
|
||||
@@ -349,7 +388,7 @@ export const JobsView: React.FC<JobsViewProps> = ({ jobs, clients, onRefresh, se
|
||||
<h4 style={{ fontSize: '0.9rem', fontWeight: 600, color: 'var(--accent-cyan)', marginBottom: '12px' }}>
|
||||
Filtros Avanzados (Estilo Karen's Replicator)
|
||||
</h4>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr', gap: '12px', marginBottom: '12px' }}>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '12px', marginBottom: '12px' }}>
|
||||
<div className="form-group">
|
||||
<label>Filtros de Exclusión (Separados por coma):</label>
|
||||
<input
|
||||
@@ -360,6 +399,16 @@ export const JobsView: React.FC<JobsViewProps> = ({ jobs, clients, onRefresh, se
|
||||
onChange={(e) => setFormData({ ...formData, exclude_patterns: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>Ruta Copia Local Duplicada (Opcional):</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-input"
|
||||
placeholder="Ej: D:\RespaldosLocal o \\NAS\backup"
|
||||
value={formData.local_destination_path}
|
||||
onChange={(e) => setFormData({ ...formData, local_destination_path: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: '18px', marginBottom: '16px' }}>
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: '6px', fontSize: '0.84rem', cursor: 'pointer' }}>
|
||||
@@ -447,6 +496,83 @@ export const JobsView: React.FC<JobsViewProps> = ({ jobs, clients, onRefresh, se
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* Job Runs History Modal (Phase 4) */}
|
||||
{showRunsModal && selectedJobForRuns && (
|
||||
<div className="modal-backdrop">
|
||||
<div className="modal-content" style={{ maxWidth: '820px', maxHeight: '80vh', overflowY: 'auto' }}>
|
||||
<div className="modal-header">
|
||||
<h4 style={{ fontSize: '1.1rem', fontWeight: 700 }}>
|
||||
Historial de Ejecuciones — {selectedJobForRuns.name} ({selectedJobForRuns.job_code})
|
||||
</h4>
|
||||
<button className="modal-close" onClick={() => setShowRunsModal(false)}>
|
||||
<X size={18} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div style={{ padding: '16px 0' }}>
|
||||
{loadingRuns ? (
|
||||
<div style={{ textAlign: 'center', padding: '20px', color: 'var(--text-dim)' }}>Cargando historial...</div>
|
||||
) : runsHistory.length === 0 ? (
|
||||
<div style={{ textAlign: 'center', padding: '20px', color: 'var(--text-dim)' }}>No hay ejecuciones registradas para este trabajo todavía.</div>
|
||||
) : (
|
||||
<div style={{ overflowX: 'auto' }}>
|
||||
<table className="table" style={{ fontSize: '0.82rem', width: '100%' }}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Fecha de Inicio</th>
|
||||
<th>Duración / Fin</th>
|
||||
<th>Estado</th>
|
||||
<th style={{ textAlign: 'center' }}>Escaneados</th>
|
||||
<th style={{ textAlign: 'center' }}>Copiados</th>
|
||||
<th style={{ textAlign: 'center' }}>Omitidos</th>
|
||||
<th style={{ textAlign: 'center' }}>Errores</th>
|
||||
<th>Transferido</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{runsHistory.map((run) => {
|
||||
const start = new Date(run.started_at);
|
||||
const end = run.completed_at ? new Date(run.completed_at) : null;
|
||||
const durationSecs = end ? Math.round((end.getTime() - start.getTime()) / 1000) : 0;
|
||||
|
||||
return (
|
||||
<tr key={run.id}>
|
||||
<td>{start.toLocaleString()}</td>
|
||||
<td>
|
||||
{end ? `${durationSecs}s (${end.toLocaleTimeString()})` : <span style={{ color: 'var(--accent-cyan)' }}>En ejecución...</span>}
|
||||
</td>
|
||||
<td>
|
||||
<span className={`badge ${run.status === 'SUCCESS' ? 'badge-online' : run.status === 'RUNNING' ? 'badge-syncing' : 'badge-offline'}`}>
|
||||
{run.status}
|
||||
</span>
|
||||
{run.error_summary && (
|
||||
<div style={{ fontSize: '0.72rem', color: '#fda4af', marginTop: '4px', maxWidth: '200px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }} title={run.error_summary}>
|
||||
{run.error_summary}
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
<td style={{ textAlign: 'center' }}>{run.files_scanned}</td>
|
||||
<td style={{ textAlign: 'center', color: '#60a5fa' }}>{run.files_copied}</td>
|
||||
<td style={{ textAlign: 'center', color: '#a7f3d0' }}>{run.files_skipped}</td>
|
||||
<td style={{ textAlign: 'center', color: run.errors_count > 0 ? '#f43f5e' : 'inherit' }}>{run.errors_count}</td>
|
||||
<td>{(run.bytes_transferred / (1024 * 1024)).toFixed(2)} MB</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="modal-footer" style={{ borderTop: '1px solid rgba(255,255,255,0.08)', paddingTop: '12px' }}>
|
||||
<button className="btn btn-secondary" onClick={() => setShowRunsModal(false)}>
|
||||
Cerrar
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -48,6 +48,7 @@ export interface BackupJobItem {
|
||||
skip_system: boolean;
|
||||
skip_readonly: boolean;
|
||||
sync_deletions: boolean;
|
||||
local_destination_path?: string | null;
|
||||
schedule_cron: string;
|
||||
is_active: boolean;
|
||||
keep_daily: number;
|
||||
@@ -60,6 +61,21 @@ export interface BackupJobItem {
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface JobRunItem {
|
||||
id: number;
|
||||
job_id: number;
|
||||
client_id: number;
|
||||
started_at: string;
|
||||
completed_at?: string;
|
||||
status: string;
|
||||
files_scanned: number;
|
||||
files_copied: number;
|
||||
files_skipped: number;
|
||||
errors_count: number;
|
||||
bytes_transferred: number;
|
||||
error_summary?: string;
|
||||
}
|
||||
|
||||
export interface BackupFileItem {
|
||||
id: number;
|
||||
client_id: number;
|
||||
@@ -200,6 +216,7 @@ export const api = {
|
||||
skip_system: boolean;
|
||||
skip_readonly: boolean;
|
||||
sync_deletions: boolean;
|
||||
local_destination_path?: string | null;
|
||||
schedule_cron: string;
|
||||
keep_daily: number;
|
||||
keep_weekly: number;
|
||||
@@ -220,6 +237,7 @@ export const api = {
|
||||
skip_system: boolean;
|
||||
skip_readonly: boolean;
|
||||
sync_deletions: boolean;
|
||||
local_destination_path?: string | null;
|
||||
schedule_cron: string;
|
||||
keep_daily: number;
|
||||
keep_weekly: number;
|
||||
@@ -235,6 +253,8 @@ export const api = {
|
||||
request<{ message: string }>(`/jobs/${jobId}/trigger`, { method: 'POST' }),
|
||||
deleteJob: (jobId: number) =>
|
||||
request<{ message: string }>(`/jobs/${jobId}`, { method: 'DELETE' }),
|
||||
getJobRuns: (jobId: number) =>
|
||||
request<JobRunItem[]>(`/jobs/${jobId}/runs`),
|
||||
|
||||
// Backups
|
||||
getBackups: (clientId?: number, jobId?: number) => {
|
||||
|
||||
Reference in New Issue
Block a user