Initial project commit: Docker + SvelteKit source

This commit is contained in:
2026-02-09 10:32:51 -07:00
parent fafb3d70fe
commit 7e7773578d
20 changed files with 6077 additions and 0 deletions

346
src/routes/+page.svelte Normal file
View File

@@ -0,0 +1,346 @@
<script lang="ts">
import Navbar from '$lib/components/Navbar.svelte';
import Sidebar from '$lib/components/Sidebar.svelte';
import { onMount } from 'svelte';
let { data } = $props();
let activeSection = $state('summary');
// Extract data from load function
const {
databaseRows, summaryMain, restoredCount, notRestoredCount,
databaseRowsAZ, summaryAZ, backupFiles, clientsData,
alertsData, basesDeDatosList,
errors // Capturar errores del servidor
} = data;
// Derived Metrics
const totalDatabases = summaryMain?.total_databases || 0;
const totalSizeGB = summaryMain?.total_size_gb ? parseFloat(summaryMain.total_size_gb).toFixed(2) : '0';
// Disk space placeholders (passed from server or calculated)
const diskC = "Free"; // Implement proper disk check
const diskD = "Free";
onMount(() => {
// Initialize DataTables
// Note: In Svelte, we typically avoid jQuery, but for migration compatibility
// with the specific glass theme and plugins, we initialize them here.
// Ensure scripts in app.html are loaded.
// Use a timeout to ensure DOM is ready and scripts are loaded
setTimeout(() => {
if (typeof window.jQuery !== 'undefined') {
const jq = window.jQuery;
// Initialize tables only if data exists
if (databaseRows && databaseRows.length > 0) jq('#combinedTable').DataTable({ responsive: true });
if (databaseRowsAZ && databaseRowsAZ.length > 0) jq('#combinedTable1').DataTable({ responsive: true });
if (backupFiles && backupFiles.length > 0) jq('#backupTable').DataTable({ responsive: true });
if (clientsData && clientsData.length > 0) jq('#clientTable').DataTable({ responsive: true });
if (alertsData && alertsData.length > 0) jq('#alertsTable').DataTable({ responsive: true });
if (basesDeDatosList && basesDeDatosList.length > 0) jq('#basesDeDatosTable').DataTable({ responsive: true });
}
}, 100);
});
</script>
<div class="d-flex min-vh-100">
<Navbar />
<Sidebar bind:activeSection />
<!-- Main Content -->
<div class="md-content-wrapper">
<!-- Header Section -->
<div class="d-flex justify-content-between align-items-center mb-5" data-aos="fade-down">
<div>
<h1 class="page-title">Panel de Control</h1>
<p class="text-muted fs-5 mb-0">Sistema de Gestión de Bases de Datos</p>
</div>
<div class="text-end">
<div class="text-muted small">{new Date().toLocaleDateString('es-ES')}</div>
<div class="fw-bold" id="current-time">{new Date().toLocaleTimeString('es-ES')}</div>
</div>
</div>
<!-- Global Alert Area -->
{#if errors}
{#if errors.primary}
<div class="alert alert-danger d-flex align-items-center mb-4 shadow-sm" role="alert">
<span class="material-icons-outlined me-2">error</span>
<div>
<strong>Error Servidor Principal:</strong> {errors.primary}
</div>
</div>
{/if}
{#if errors.secondary}
<div class="alert alert-warning d-flex align-items-center mb-4 shadow-sm" role="alert">
<span class="material-icons-outlined me-2">warning</span>
<div>
<strong>Advertencia Servidor Secundario:</strong> {errors.secondary}
<br><small>Algunos datos de clientes o catálogos podrían no estar disponibles.</small>
</div>
</div>
{/if}
{#if errors.azure}
<div class="alert alert-info d-flex align-items-center mb-4 shadow-sm" role="alert">
<span class="material-icons-outlined me-2">cloud_off</span>
<div>
<strong>Aviso Azure:</strong> {errors.azure}
</div>
</div>
{/if}
{#if errors.backups}
<div class="alert alert-secondary d-flex align-items-center mb-4 shadow-sm" role="alert">
<span class="material-icons-outlined me-2">folder_off</span>
<div>
<strong>Sistema de Archivos:</strong> {errors.backups}
</div>
</div>
{/if}
{/if}
{#if activeSection === 'summary'}
<!-- Summary Section -->
<section id="summarySection">
<!-- Metrics -->
<div class="row g-4 mb-5">
<div class="col-xl-3 col-lg-6">
<div class="md-metric-card">
<div class="material-icons-outlined md-metric-icon">storage</div>
<div class="md-metric-value">{totalDatabases}</div>
<div class="md-metric-label">Bases de Datos Totales</div>
</div>
</div>
<!-- More metrics... simplified for brevity, assume similar structure -->
<div class="col-xl-3 col-lg-6">
<div class="md-metric-card">
<div class="material-icons-outlined md-metric-icon" style="color: var(--md-success)">cloud_done</div>
<div class="md-metric-value" style="color: var(--md-success)">{totalSizeGB} GB</div>
<div class="md-metric-label">Almacenamiento Utilizado</div>
</div>
</div>
<div class="col-xl-3 col-lg-6">
<div class="md-metric-card">
<div class="material-icons-outlined md-metric-icon" style="color: var(--md-warning)">restore</div>
<div class="md-metric-value" style="color: var(--md-warning)">{notRestoredCount}</div>
<div class="md-metric-label">Pendientes de Restaurar</div>
</div>
</div>
<div class="col-xl-3 col-lg-6">
<div class="md-metric-card">
<div class="material-icons-outlined md-metric-icon" style="color: var(--md-info)">check_circle</div>
<div class="md-metric-value" style="color: var(--md-info)">{restoredCount}</div>
<div class="md-metric-label">Restauradas 24h</div>
</div>
</div>
</div>
<!-- Table -->
<div class="md-card">
<h4 class="md-card-title mb-4">Estado de Bases de Datos Local</h4>
<div class="table-responsive">
<table id="combinedTable" class="table display responsive nowrap" style="width:100%">
<thead>
<tr>
<th>Cliente</th>
<th>Original</th>
<th>Tamaño</th>
<th>Restauración</th>
<th>Estado</th>
</tr>
</thead>
<tbody>
{#each (databaseRows || []) as row}
<tr>
<td>{row.visible_name}</td>
<td>{row.original_name}</td>
<td>{row.size_mb ? Number(row.size_mb).toFixed(2) : 0} MB</td>
<td>{row.last_restore_date ? new Date(row.last_restore_date).toLocaleDateString() : 'N/A'}</td>
<td>
{#if row.last_restore_date && new Date(row.last_restore_date) > new Date(Date.now() - 86400000)}
<span class="badge bg-success">Actualizada</span>
{:else}
<span class="badge bg-danger">Atención</span>
{/if}
</td>
</tr>
{:else}
<tr>
<td colspan="5" class="text-center text-muted py-4">
No hay datos disponibles o error de conexión.
</td>
</tr>
{/each}
</tbody>
</table>
</div>
</div>
</section>
{/if}
{#if activeSection === 'azure'}
<!-- Azure Section -->
<section id="AzureSection">
<div class="md-card">
<h4 class="md-card-title mb-4">Azure Cloud Databases</h4>
<p class="text-muted mb-4">Instancias alojadas en servicios de nube.</p>
<div class="table-responsive">
<table id="combinedTable1" class="table w-100">
<thead>
<tr>
<th>Nombre</th>
<th>Nombre Original</th>
<th>Tamaño</th>
<th>Última Sync</th>
</tr>
</thead>
<tbody>
{#each (databaseRowsAZ || []) as row}
<tr>
<td>{row.visible_name}</td>
<td>{row.original_name}</td>
<td>{row.size_mb ? Number(row.size_mb).toFixed(2) : 0} MB</td>
<td>{row.last_restore_date ? new Date(row.last_restore_date).toLocaleDateString() : 'N/A'}</td>
</tr>
{:else}
<tr>
<td colspan="4" class="text-center text-muted py-4">
No hay datos de Azure disponibles.
</td>
</tr>
{/each}
</tbody>
</table>
</div>
</div>
</section>
{/if}
{#if activeSection === 'backups'}
<section id="backupSection">
<div class="md-card">
<h4 class="md-card-title mb-4">Respaldos Almacenados</h4>
<p class="text-muted mb-4">Archivos físicos encontrados en la ruta de respaldo.</p>
<table id="backupTable" class="table w-100">
<thead><tr><th>Archivo</th><th>Cliente</th><th>Fecha</th><th>Tamaño</th></tr></thead>
<tbody>
{#each (backupFiles || []) as file}
<tr>
<td>{file.name}</td>
<td>{file.client_name}</td>
<td>{new Date(file.date).toLocaleString()}</td>
<td>{file.size}</td>
</tr>
{:else}
<tr>
<td colspan="4" class="text-center text-muted py-4">
No se encontraron archivos de respaldo o no hay acceso al disco.
</td>
</tr>
{/each}
</tbody>
</table>
</div>
</section>
{/if}
{#if activeSection === 'clients'}
<section id="clientsSection">
<div class="md-card">
<h4 class="md-card-title mb-4">Catálogo de Clientes</h4>
<table id="clientTable" class="table w-100">
<thead><tr><th>ID</th><th>Nombre</th><th>Email</th><th>Estado</th></tr></thead>
<tbody>
{#each (clientsData || []) as client}
<tr>
<td>{client.ID}</td>
<td>{client.Nombre}</td>
<td>{client.CorreoNotificacion}</td>
<td>
{#if client.Activo}
<span class="badge bg-success">Activo</span>
{:else}
<span class="badge bg-secondary">Inactivo</span>
{/if}
</td>
</tr>
{:else}
<tr>
<td colspan="4" class="text-center text-muted py-4">
No hay datos de clientes (Verifique conexión secundaria).
</td>
</tr>
{/each}
</tbody>
</table>
</div>
</section>
{/if}
{#if activeSection === 'alerts'}
<section id="alertsSection">
<div class="md-card">
<h4 class="md-card-title mb-4">Alertas Críticas</h4>
<div class="alert alert-warning mb-4">
<span class="material-icons-outlined align-middle fs-5 me-2">info</span>
Mostrando bases de datos que no se han restaurado en los últimos 2 días.
</div>
<table id="alertsTable" class="table w-100">
<thead><tr><th>Base de Datos</th><th>Última Restauración</th><th>Severidad</th></tr></thead>
<tbody>
{#each (alertsData || []) as alert}
<tr>
<td>{alert.visible_name}</td>
<td>{alert.last_restore_date ? new Date(alert.last_restore_date).toLocaleString() : 'Nunca'}</td>
<td><span class="badge bg-danger">Crítica</span></td>
</tr>
{:else}
<tr>
<td colspan="3" class="text-center text-muted py-4">
¡Excelente! No hay alertas críticas pendientes.
</td>
</tr>
{/each}
</tbody>
</table>
</div>
</section>
{/if}
{#if activeSection === 'databases'}
<section id="databasesSection">
<div class="md-card">
<h4 class="md-card-title mb-4">Gestión de Bases de Datos</h4>
<table id="basesDeDatosTable" class="table w-100">
<thead><tr><th>ID</th><th>Nodo</th><th>Nombre</th><th>Servidor</th></tr></thead>
<tbody>
{#each (basesDeDatosList || []) as bd}
<tr>
<td>{bd.ID}</td>
<td>{bd.NodoSubNodo}</td>
<td>{bd.Nombre}</td>
<td>{bd.ServerName}</td>
</tr>
{:else}
<tr>
<td colspan="4" class="text-center text-muted py-4">
No hay registros de gestión disponibles.
</td>
</tr>
{/each}
</tbody>
</table>
</div>
</section>
{/if}
</div>
</div>