Initial project commit: Docker + SvelteKit source
This commit is contained in:
7
.dockerignore
Normal file
7
.dockerignore
Normal file
@@ -0,0 +1,7 @@
|
||||
node_modules
|
||||
.svelte-kit
|
||||
build
|
||||
.env
|
||||
.git
|
||||
.DS_Store
|
||||
npm-debug.log
|
||||
9
.github/copilot-instructions.md
vendored
Normal file
9
.github/copilot-instructions.md
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
- [ ] Checklist: Create SvelteKit Project
|
||||
- [x] Clarify Project Requirements
|
||||
- [ ] Scaffold the Project
|
||||
- [ ] Customize the Project
|
||||
- [ ] Install Required Extensions
|
||||
- [ ] Compile the Project
|
||||
- [ ] Create and Run Task
|
||||
- [ ] Launch the Project
|
||||
- [ ] Ensure Documentation is Complete
|
||||
13
.gitignore
vendored
Normal file
13
.gitignore
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
# Node
|
||||
node_modules
|
||||
.DS_Store
|
||||
|
||||
# SvelteKit
|
||||
.svelte-kit
|
||||
build
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# Docker
|
||||
docker-compose.override.yml
|
||||
16
.vscode/tasks.json
vendored
Normal file
16
.vscode/tasks.json
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "Run Dev Server",
|
||||
"type": "shell",
|
||||
"command": "npm run dev",
|
||||
"args": [],
|
||||
"isBackground": true,
|
||||
"problemMatcher": [
|
||||
"$tsc-watch"
|
||||
],
|
||||
"group": "build"
|
||||
}
|
||||
]
|
||||
}
|
||||
33
Dockerfile
Normal file
33
Dockerfile
Normal file
@@ -0,0 +1,33 @@
|
||||
# Stage 1: Build
|
||||
FROM node:20-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy package files
|
||||
COPY package*.json ./
|
||||
|
||||
# Install dependencies
|
||||
RUN npm ci
|
||||
|
||||
# Copy source code
|
||||
COPY . .
|
||||
|
||||
# Build the application
|
||||
RUN npm run build
|
||||
RUN npm prune --production
|
||||
|
||||
# Stage 2: Run
|
||||
FROM node:20-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy built application and dependencies
|
||||
COPY --from=builder /app/build build/
|
||||
COPY --from=builder /app/node_modules node_modules/
|
||||
COPY package.json .
|
||||
|
||||
# Expose port
|
||||
EXPOSE 3000
|
||||
|
||||
# Start the application
|
||||
CMD ["node", "build"]
|
||||
34
docker-compose.yml
Normal file
34
docker-compose.yml
Normal file
@@ -0,0 +1,34 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
app:
|
||||
build: .
|
||||
ports:
|
||||
- "3000:3000"
|
||||
environment:
|
||||
# Use host.docker.internal to access resources on the host machine
|
||||
- DB_PRIMARY_HOST=host.docker.internal
|
||||
- DB_PRIMARY_USER=${DB_PRIMARY_USER}
|
||||
- DB_PRIMARY_PASS=${DB_PRIMARY_PASS}
|
||||
- DB_PRIMARY_DB=${DB_PRIMARY_DB}
|
||||
|
||||
- DB_SECONDARY_HOST=host.docker.internal
|
||||
- DB_SECONDARY_USER=${DB_SECONDARY_USER}
|
||||
- DB_SECONDARY_PASS=${DB_SECONDARY_PASS}
|
||||
- DB_SECONDARY_DB=${DB_SECONDARY_DB}
|
||||
|
||||
- DB_AZURE_HOST=${DB_AZURE_HOST}
|
||||
- DB_AZURE_USER=${DB_AZURE_USER}
|
||||
- DB_AZURE_PASS=${DB_AZURE_PASS}
|
||||
- DB_AZURE_DB=${DB_AZURE_DB}
|
||||
|
||||
# Use a mounted volume for backups
|
||||
- BACKUP_PATH=/data/backups
|
||||
|
||||
- PORT=3000
|
||||
- ORIGIN=http://localhost:3000
|
||||
volumes:
|
||||
# Map a local folder to the container's backup path
|
||||
- ./backups:/data/backups:ro
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
4826
package-lock.json
generated
Normal file
4826
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
30
package.json
Normal file
30
package.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"name": "transmitiras-dashboard",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"check": "svelte-kit sync && tsc --noEmit"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/adapter-auto": "^3.3.1",
|
||||
"@sveltejs/adapter-node": "^5.5.2",
|
||||
"@sveltejs/kit": "^2.0.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^4.0.0-next.0",
|
||||
"@types/mssql": "^9.1.5",
|
||||
"@types/node": "^22.0.0",
|
||||
"svelte": "^5.0.0-next.1",
|
||||
"svelte-check": "^3.6.0",
|
||||
"tslib": "^2.4.1",
|
||||
"typescript": "^5.0.0",
|
||||
"vite": "^5.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"bootstrap": "^5.3.3",
|
||||
"dotenv": "^16.4.5",
|
||||
"mssql": "^10.0.2"
|
||||
},
|
||||
"type": "module"
|
||||
}
|
||||
196
src/app.css
Normal file
196
src/app.css
Normal file
@@ -0,0 +1,196 @@
|
||||
:root {
|
||||
/* Google Material Colors */
|
||||
--md-primary: #1a73e8;
|
||||
--md-primary-hover: #1557b0;
|
||||
--md-secondary: #5f6368;
|
||||
--md-success: #1e8e3e;
|
||||
--md-danger: #d93025;
|
||||
--md-warning: #f9ab00;
|
||||
|
||||
--md-bg: #f8f9fa; /* Light gray background */
|
||||
--md-surface: #ffffff;
|
||||
|
||||
--md-text-primary: #202124;
|
||||
--md-text-secondary: #5f6368;
|
||||
|
||||
/* Elevation Shadows */
|
||||
--shadow-1: 0 1px 2px 0 rgba(60,64,67,0.3), 0 1px 3px 1px rgba(60,64,67,0.15);
|
||||
--shadow-2: 0 1px 3px 0 rgba(60,64,67,0.3), 0 4px 8px 3px rgba(60,64,67,0.15);
|
||||
|
||||
--border-radius: 8px;
|
||||
--transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Inter', Roboto, sans-serif;
|
||||
background-color: var(--md-bg);
|
||||
color: var(--md-text-primary);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
color: var(--md-text-primary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Navbar */
|
||||
.md-navbar {
|
||||
background: var(--md-surface);
|
||||
height: 64px;
|
||||
box-shadow: var(--shadow-1);
|
||||
z-index: 1040;
|
||||
}
|
||||
|
||||
.brand-text {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 500;
|
||||
color: var(--md-text-secondary);
|
||||
}
|
||||
|
||||
.brand-logo {
|
||||
color: var(--md-primary);
|
||||
}
|
||||
|
||||
/* Sidebar */
|
||||
.md-sidebar {
|
||||
background: var(--md-surface);
|
||||
width: 260px;
|
||||
height: 100vh;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 1030;
|
||||
padding-top: 64px; /* Matches navbar height */
|
||||
border-right: none; /* Shadow handles separation */
|
||||
box-shadow: 1px 0 0 rgba(0,0,0,0.12); /* Subtle divider */
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.md-nav-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 24px;
|
||||
height: 48px;
|
||||
color: var(--md-text-secondary);
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
font-size: 0.875rem;
|
||||
border-radius: 0 24px 24px 0; /* Material pill shape on one side or full pill */
|
||||
margin: 4px 12px 4px 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.md-nav-link:hover {
|
||||
background-color: #f1f3f4;
|
||||
color: var(--md-text-primary);
|
||||
}
|
||||
|
||||
.md-nav-link.active {
|
||||
background-color: #e8f0fe;
|
||||
color: var(--md-primary);
|
||||
}
|
||||
|
||||
.md-nav-icon {
|
||||
margin-right: 16px;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
/* Main Content */
|
||||
.md-content-wrapper {
|
||||
margin-left: 260px;
|
||||
padding: 24px;
|
||||
padding-top: 88px; /* 64px navbar + 24px spacing */
|
||||
min-height: 100vh;
|
||||
transition: margin-left 0.3s ease;
|
||||
}
|
||||
|
||||
/* Cards */
|
||||
.md-card {
|
||||
background: var(--md-surface);
|
||||
border-radius: var(--border-radius);
|
||||
padding: 24px;
|
||||
box-shadow: var(--shadow-1);
|
||||
margin-bottom: 24px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.md-card-title {
|
||||
font-size: 1.125rem;
|
||||
color: var(--md-text-primary);
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
/* Metrics */
|
||||
.md-metric-card {
|
||||
background: var(--md-surface);
|
||||
border-radius: var(--border-radius);
|
||||
padding: 24px;
|
||||
box-shadow: var(--shadow-1);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.md-metric-value {
|
||||
font-size: 2.25rem;
|
||||
font-weight: 400;
|
||||
color: var(--md-text-primary);
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
.md-metric-label {
|
||||
color: var(--md-text-secondary);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.md-metric-icon {
|
||||
color: var(--md-primary);
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
/* Tables */
|
||||
table.dataTable {
|
||||
width: 100% !important;
|
||||
border-collapse: collapse !important;
|
||||
}
|
||||
|
||||
table.dataTable thead th {
|
||||
border-bottom: 1px solid rgba(0,0,0,0.12) !important;
|
||||
color: var(--md-text-secondary);
|
||||
font-weight: 500;
|
||||
font-size: 0.75rem;
|
||||
text-transform: uppercase;
|
||||
padding: 16px !important;
|
||||
}
|
||||
|
||||
table.dataTable tbody td {
|
||||
padding: 16px !important;
|
||||
border-bottom: 1px solid rgba(0,0,0,0.06);
|
||||
color: var(--md-text-primary);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 992px) {
|
||||
.md-sidebar {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
.md-sidebar.show {
|
||||
transform: translateX(0);
|
||||
box-shadow: var(--shadow-2);
|
||||
}
|
||||
.md-content-wrapper {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
17
src/app.d.ts
vendored
Normal file
17
src/app.d.ts
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
// See https://kit.svelte.dev/docs/types#app
|
||||
// for information about these interfaces
|
||||
declare global {
|
||||
namespace App {
|
||||
// interface Error {}
|
||||
// interface Locals {}
|
||||
// interface PageData {}
|
||||
// interface Platform {}
|
||||
}
|
||||
|
||||
interface Window {
|
||||
jQuery: any;
|
||||
$: any;
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
||||
47
src/app.html
Normal file
47
src/app.html
Normal file
@@ -0,0 +1,47 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
|
||||
<!-- Fuentes Premium -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
|
||||
<!-- CSS Frameworks -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="https://cdn.datatables.net/1.13.7/css/dataTables.bootstrap5.min.css">
|
||||
<link rel="stylesheet" href="https://cdn.datatables.net/buttons/2.4.2/css/buttons.bootstrap5.min.css">
|
||||
<link rel="stylesheet" href="https://cdn.datatables.net/responsive/2.5.0/css/responsive.bootstrap5.min.css">
|
||||
|
||||
<!-- Iconos -->
|
||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons+Outlined" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
|
||||
|
||||
<!-- Animaciones -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/aos@2.3.4/dist/aos.css">
|
||||
|
||||
<!-- Scripts (DataTables requires jQuery) -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/aos@2.3.4/dist/aos.js"></script>
|
||||
|
||||
<!-- DataTables Scripts -->
|
||||
<script src="https://cdn.datatables.net/1.13.7/js/jquery.dataTables.min.js"></script>
|
||||
<script src="https://cdn.datatables.net/1.13.7/js/dataTables.bootstrap5.min.js"></script>
|
||||
<script src="https://cdn.datatables.net/buttons/2.4.2/js/dataTables.buttons.min.js"></script>
|
||||
<script src="https://cdn.datatables.net/buttons/2.4.2/js/buttons.bootstrap5.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
|
||||
<script src="https://cdn.datatables.net/buttons/2.4.2/js/buttons.html5.min.js"></script>
|
||||
<script src="https://cdn.datatables.net/buttons/2.4.2/js/buttons.print.min.js"></script>
|
||||
<script src="https://cdn.datatables.net/responsive/2.5.0/js/dataTables.responsive.min.js"></script>
|
||||
<script src="https://cdn.datatables.net/responsive/2.5.0/js/responsive.bootstrap5.min.js"></script>
|
||||
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
</body>
|
||||
</html>
|
||||
38
src/lib/components/Navbar.svelte
Normal file
38
src/lib/components/Navbar.svelte
Normal file
@@ -0,0 +1,38 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
</script>
|
||||
|
||||
<nav class="navbar navbar-expand-lg md-navbar position-fixed w-100 top-0">
|
||||
<div class="container-fluid px-4">
|
||||
<button class="btn sidebar-toggle d-lg-none me-3" type="button" onclick={() => document.getElementById('sidebar')?.classList.toggle('show')}>
|
||||
<i class="bi bi-list fs-4"></i>
|
||||
</button>
|
||||
|
||||
<a class="navbar-brand d-flex align-items-center" href="/">
|
||||
<div class="md-nav-icon me-3 brand-logo" style="width: 40px; height: 40px; font-size: 1.2rem; display: flex; align-items: center; justify-content: center;">
|
||||
<i class="material-icons-outlined">storage</i>
|
||||
</div>
|
||||
<span class="brand-text d-none d-sm-block">Database Management System</span>
|
||||
</a>
|
||||
|
||||
<div class="navbar-nav ms-auto d-flex align-items-center">
|
||||
<div class="nav-item dropdown me-3">
|
||||
<a class="nav-link dropdown-toggle px-3" href="#" id="userDropdown" role="button" data-bs-toggle="dropdown">
|
||||
<i class="bi bi-person-circle me-2"></i>
|
||||
<span class="d-none d-md-inline">Admin</span>
|
||||
</a>
|
||||
<ul class="dropdown-menu dropdown-menu-end md-card border-0 p-2">
|
||||
<li><a class="dropdown-item rounded" href="#"><i class="bi bi-person me-2"></i>Perfil</a></li>
|
||||
<li><a class="dropdown-item rounded" href="#"><i class="bi bi-gear me-2"></i>Configuración</a></li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li><a class="dropdown-item text-danger rounded" href="/logout"><i class="bi bi-box-arrow-right me-2"></i>Cerrar Sesión</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<a href="/logout" class="btn btn-primary d-flex align-items-center">
|
||||
<i class="bi bi-box-arrow-right me-2"></i>
|
||||
<span class="d-none d-md-inline">Salir</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
79
src/lib/components/Sidebar.svelte
Normal file
79
src/lib/components/Sidebar.svelte
Normal file
@@ -0,0 +1,79 @@
|
||||
<script lang="ts">
|
||||
interface Props {
|
||||
activeSection: string;
|
||||
}
|
||||
|
||||
let { activeSection = $bindable('summary') }: Props = $props();
|
||||
|
||||
function setActive(section: string) {
|
||||
activeSection = section;
|
||||
}
|
||||
</script>
|
||||
|
||||
<aside class="md-sidebar" id="sidebar">
|
||||
<div class="px-3 py-4">
|
||||
<div class="text-center mb-4 d-flex align-items-center px-3">
|
||||
<div style="font-size: 2rem; color: var(--md-primary)" class="me-2">
|
||||
<i class="material-icons-outlined">dashboard</i>
|
||||
</div>
|
||||
<div class="text-start">
|
||||
<h4 class="brand-text mb-0" style="font-size: 1.1rem; color: var(--md-text-primary)">TransmitirAS</h4>
|
||||
<p class="text-muted small mb-0" style="font-size: 0.75rem">Management System</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav class="nav flex-column">
|
||||
<button class="md-nav-link {activeSection === 'summary' ? 'active' : ''}" onclick={() => setActive('summary')}>
|
||||
<i class="bi bi-speedometer2 md-nav-icon"></i>
|
||||
<span>Panel Principal</span>
|
||||
</button>
|
||||
|
||||
<button class="md-nav-link {activeSection === 'backups' ? 'active' : ''}" onclick={() => setActive('backups')}>
|
||||
<i class="bi bi-hdd-network md-nav-icon"></i>
|
||||
<span>Respaldos Almacenados</span>
|
||||
</button>
|
||||
|
||||
<button class="md-nav-link {activeSection === 'clients' ? 'active' : ''}" onclick={() => setActive('clients')}>
|
||||
<i class="bi bi-people md-nav-icon"></i>
|
||||
<span>Catálogo de Clientes</span>
|
||||
</button>
|
||||
|
||||
<button class="md-nav-link {activeSection === 'alerts' ? 'active' : ''}" onclick={() => setActive('alerts')}>
|
||||
<i class="bi bi-exclamation-triangle md-nav-icon"></i>
|
||||
<span>Alertas Críticas</span>
|
||||
</button>
|
||||
|
||||
<button class="md-nav-link {activeSection === 'azure' ? 'active' : ''}" onclick={() => setActive('azure')}>
|
||||
<i class="bi bi-cloud md-nav-icon"></i>
|
||||
<span>Azure Cloud</span>
|
||||
</button>
|
||||
|
||||
<button class="md-nav-link {activeSection === 'databases' ? 'active' : ''}" onclick={() => setActive('databases')}>
|
||||
<i class="bi bi-database md-nav-icon"></i>
|
||||
<span>Gestión de Bases de Datos</span>
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
<div class="mt-5 px-3">
|
||||
<h6 class="text-uppercase text-muted small fw-bold mb-3 ms-2">Configuración</h6>
|
||||
<button class="md-nav-link" onclick={() => console.log('Config')}>
|
||||
<i class="bi bi-gear md-nav-icon"></i>
|
||||
<span>Configuración</span>
|
||||
</button>
|
||||
<a href="/api-docs" class="md-nav-link text-decoration-none">
|
||||
<i class="bi bi-code-square md-nav-icon"></i>
|
||||
<span>API</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="mt-auto px-4 pt-4 text-center">
|
||||
<div class="d-flex align-items-center justify-content-center text-success mb-2">
|
||||
<i class="bi bi-shield-check me-2"></i>
|
||||
<small class="fw-bold">Sistema Seguro</small>
|
||||
</div>
|
||||
<div class="text-muted small" style="font-size: 0.7rem;">
|
||||
{new Date().toLocaleDateString()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
62
src/lib/server/db.ts
Normal file
62
src/lib/server/db.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import sql from 'mssql';
|
||||
import { env } from '$env/dynamic/private';
|
||||
|
||||
const primaryConfig: sql.config = {
|
||||
user: env.DB_PRIMARY_USER,
|
||||
password: env.DB_PRIMARY_PASS,
|
||||
server: env.DB_PRIMARY_HOST,
|
||||
database: env.DB_PRIMARY_DB,
|
||||
options: {
|
||||
encrypt: true, // For Azure/Remote
|
||||
trustServerCertificate: true // Self-signed certs
|
||||
}
|
||||
};
|
||||
|
||||
const secondaryConfig: sql.config = {
|
||||
user: env.DB_SECONDARY_USER,
|
||||
password: env.DB_SECONDARY_PASS,
|
||||
server: env.DB_SECONDARY_HOST,
|
||||
database: env.DB_SECONDARY_DB,
|
||||
options: {
|
||||
encrypt: true,
|
||||
trustServerCertificate: true
|
||||
}
|
||||
};
|
||||
|
||||
const azureConfig: sql.config = {
|
||||
user: env.DB_AZURE_USER,
|
||||
password: env.DB_AZURE_PASS,
|
||||
server: env.DB_AZURE_HOST,
|
||||
database: env.DB_AZURE_DB,
|
||||
options: {
|
||||
encrypt: true,
|
||||
trustServerCertificate: true
|
||||
}
|
||||
};
|
||||
|
||||
class Database {
|
||||
private primaryPool: sql.ConnectionPool | null = null;
|
||||
private secondaryPool: sql.ConnectionPool | null = null;
|
||||
private azurePool: sql.ConnectionPool | null = null;
|
||||
|
||||
async getPrimary(): Promise<sql.ConnectionPool> {
|
||||
if (this.primaryPool?.connected) return this.primaryPool;
|
||||
this.primaryPool = await new sql.ConnectionPool(primaryConfig).connect();
|
||||
return this.primaryPool;
|
||||
}
|
||||
|
||||
async getSecondary(): Promise<sql.ConnectionPool> {
|
||||
if (this.secondaryPool?.connected) return this.secondaryPool;
|
||||
this.secondaryPool = await new sql.ConnectionPool(secondaryConfig).connect();
|
||||
return this.secondaryPool;
|
||||
}
|
||||
|
||||
async getAzure(): Promise<sql.ConnectionPool> {
|
||||
if (this.azurePool?.connected) return this.azurePool;
|
||||
this.azurePool = await new sql.ConnectionPool(azureConfig).connect();
|
||||
return this.azurePool;
|
||||
}
|
||||
}
|
||||
|
||||
export const db = new Database();
|
||||
export { sql };
|
||||
5
src/routes/+layout.svelte
Normal file
5
src/routes/+layout.svelte
Normal file
@@ -0,0 +1,5 @@
|
||||
<script>
|
||||
import '../app.css';
|
||||
</script>
|
||||
|
||||
<slot />
|
||||
279
src/routes/+page.server.ts
Normal file
279
src/routes/+page.server.ts
Normal file
@@ -0,0 +1,279 @@
|
||||
import { db } from '$lib/server/db';
|
||||
import { env } from '$env/dynamic/private';
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import type { PageServerLoad } from './$types';
|
||||
|
||||
// Helper to check disk space (Simple Windows implementation)
|
||||
// Note: In production, consider a specialized library
|
||||
async function getDiskSpace(drive: string) {
|
||||
try {
|
||||
// Using fs.statfs if available (Node 18.15+) or just mock for now
|
||||
// Implementing proper disk check via Powershell is safer
|
||||
return { free: 0, total: 0 };
|
||||
} catch {
|
||||
return { free: 0, total: 0 };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const load: PageServerLoad = async ({ cookies }) => {
|
||||
// 1. Auth Check (Commented out for dev)
|
||||
const sessionId = cookies.get('session_id');
|
||||
// if (!sessionId) {
|
||||
// throw redirect(303, '/login');
|
||||
// }
|
||||
|
||||
// Initialize result containers
|
||||
let databaseRows: any[] = [];
|
||||
let summaryMain: any = null;
|
||||
let restoredCount = 0;
|
||||
let notRestoredCount = 0;
|
||||
|
||||
let databaseRowsAZ: any[] = [];
|
||||
let summaryAZ: any = null;
|
||||
|
||||
let backupFiles: any[] = [];
|
||||
let clientsData: any[] = [];
|
||||
let alertsData: any[] = [];
|
||||
let basesDeDatosList: any[] = [];
|
||||
|
||||
// Connection errors to be passed to UI
|
||||
let errors = {
|
||||
primary: null as string | null,
|
||||
secondary: null as string | null,
|
||||
azure: null as string | null,
|
||||
backups: null as string | null
|
||||
};
|
||||
|
||||
// --- 1. Load Primary Data (Local SQL Express) ---
|
||||
try {
|
||||
const primary = await db.getPrimary();
|
||||
|
||||
// Main Database Rows
|
||||
const queryMain = `
|
||||
SELECT
|
||||
d.name AS visible_name,
|
||||
REVERSE(SUBSTRING(REVERSE(mf.physical_name), 1, CHARINDEX('\\', REVERSE(mf.physical_name)) - 1)) AS original_name,
|
||||
SUM(mf.size * 8 / 1024) AS size_mb,
|
||||
MAX(rh.restore_date) AS last_restore_date
|
||||
FROM
|
||||
sys.databases d
|
||||
LEFT JOIN
|
||||
sys.master_files mf ON d.database_id = mf.database_id
|
||||
LEFT JOIN
|
||||
msdb.dbo.restorehistory rh ON d.name = rh.destination_database_name
|
||||
WHERE
|
||||
mf.type = 0 AND d.name != 'tempdb'
|
||||
GROUP BY
|
||||
d.name, mf.physical_name
|
||||
`;
|
||||
const resultMain = await primary.request().query(queryMain);
|
||||
databaseRows = resultMain.recordset;
|
||||
|
||||
// Calculate restored/not restored
|
||||
const oneDayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000);
|
||||
for (const row of databaseRows) {
|
||||
if (row.last_restore_date && new Date(row.last_restore_date) > oneDayAgo) {
|
||||
restoredCount++;
|
||||
} else {
|
||||
notRestoredCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// Summary Main
|
||||
const querySummary = `
|
||||
SELECT
|
||||
COUNT(DISTINCT d.database_id) AS total_databases,
|
||||
SUM(mf.size * 8 / 1024 / 1024) AS total_size_gb
|
||||
FROM
|
||||
sys.databases d
|
||||
LEFT JOIN
|
||||
sys.master_files mf ON d.database_id = mf.database_id
|
||||
WHERE
|
||||
mf.type = 0 AND d.name != 'tempdb'
|
||||
`;
|
||||
const resSummary = await primary.request().query(querySummary);
|
||||
summaryMain = resSummary.recordset[0];
|
||||
|
||||
// Alerts Data (also from Primary)
|
||||
const sqlAlerts = `
|
||||
SELECT
|
||||
d.name AS visible_name,
|
||||
MAX(rh.restore_date) AS last_restore_date
|
||||
FROM sys.databases d
|
||||
LEFT JOIN msdb.dbo.restorehistory rh ON d.name = rh.destination_database_name
|
||||
WHERE d.name != 'tempdb'
|
||||
GROUP BY d.name
|
||||
HAVING MAX(rh.restore_date) < DATEADD(DAY, -2, GETDATE()) OR MAX(rh.restore_date) IS NULL
|
||||
`;
|
||||
const resAlerts = await primary.request().query(sqlAlerts);
|
||||
alertsData = resAlerts.recordset;
|
||||
|
||||
} catch (e: any) {
|
||||
console.error("Error loading Primary DB data:", e);
|
||||
errors.primary = `Error conectando al servidor Principal: ${e.message}`;
|
||||
}
|
||||
|
||||
// --- 2. Load Azure Data ---
|
||||
try {
|
||||
// Only try connecting if we are meant to
|
||||
const azure = await db.getAzure();
|
||||
|
||||
// Re-use queryMain logic for Azure
|
||||
const queryMainAz = `
|
||||
SELECT
|
||||
d.name AS visible_name,
|
||||
REVERSE(SUBSTRING(REVERSE(mf.physical_name), 1, CHARINDEX('\\', REVERSE(mf.physical_name)) - 1)) AS original_name,
|
||||
SUM(mf.size * 8 / 1024) AS size_mb,
|
||||
MAX(rh.restore_date) AS last_restore_date
|
||||
FROM
|
||||
sys.databases d
|
||||
LEFT JOIN
|
||||
sys.master_files mf ON d.database_id = mf.database_id
|
||||
LEFT JOIN
|
||||
msdb.dbo.restorehistory rh ON d.name = rh.destination_database_name
|
||||
WHERE
|
||||
mf.type = 0 AND d.name != 'tempdb'
|
||||
GROUP BY
|
||||
d.name, mf.physical_name
|
||||
`;
|
||||
|
||||
const resultAzure = await azure.request().query(queryMainAz);
|
||||
databaseRowsAZ = resultAzure.recordset;
|
||||
|
||||
const querySummaryAz = `
|
||||
SELECT
|
||||
COUNT(DISTINCT d.database_id) AS total_databases,
|
||||
SUM(mf.size * 8 / 1024 / 1024) AS total_size_gb
|
||||
FROM
|
||||
sys.databases d
|
||||
LEFT JOIN
|
||||
sys.master_files mf ON d.database_id = mf.database_id
|
||||
WHERE
|
||||
mf.type = 0 AND d.name != 'tempdb'
|
||||
`;
|
||||
const resSummaryAz = await azure.request().query(querySummaryAz);
|
||||
summaryAZ = resSummaryAz.recordset[0];
|
||||
|
||||
} catch (e: any) {
|
||||
console.error("Error loading Azure DB data:", e);
|
||||
errors.azure = `Error conectando a Azure: ${e.message}`;
|
||||
}
|
||||
|
||||
// --- 3. Load Secondary Data (Clients & Management) ---
|
||||
// Note: We need this connection to hydrate backup info and alerts too
|
||||
let secondary: any = null;
|
||||
try {
|
||||
secondary = await db.getSecondary();
|
||||
|
||||
// Clients Catalog
|
||||
const clientsQuery = `SELECT ID, Nombre, NodoSubNodo, CorreoNotificacion, Activo, BDName FROM BasesdeDatos`;
|
||||
// Assuming 'CONTROLDESK' db context is handled in connection string or default db
|
||||
const resClients = await secondary.request().query(clientsQuery);
|
||||
clientsData = resClients.recordset;
|
||||
|
||||
// DB Management List
|
||||
const basesQueries = `SELECT ID, NodoSubNodo, Activo, RFC, Nombre, Sucursal, CorreoNotificacion, ServerName, BDName FROM BasesDeDatos`;
|
||||
const resBases = await secondary.request().query(basesQueries);
|
||||
basesDeDatosList = resBases.recordset;
|
||||
|
||||
} catch (e: any) {
|
||||
console.error("Error loading Secondary DB data:", e);
|
||||
errors.secondary = `Error conectando al servidor Secundario (ControlDesk): ${e.message}`;
|
||||
}
|
||||
|
||||
// --- 4. Process Backups & Hydrate Alerts (Requires Secondary DB) ---
|
||||
try {
|
||||
let files: string[] = [];
|
||||
try {
|
||||
files = await fs.readdir(env.BACKUP_PATH);
|
||||
} catch (e) {
|
||||
files = [];
|
||||
errors.backups = "No se pudo acceder a la carpeta de respaldos.";
|
||||
}
|
||||
|
||||
for (const file of files) {
|
||||
if (file === '.' || file === '..') continue;
|
||||
|
||||
const filePath = path.join(env.BACKUP_PATH, file);
|
||||
let stats;
|
||||
try {
|
||||
stats = await fs.stat(filePath);
|
||||
} catch { continue; }
|
||||
|
||||
const nodoName = path.parse(file).name;
|
||||
let clientData: any = null;
|
||||
|
||||
// Try to fetch metadata if secondary DB is available
|
||||
if (secondary) {
|
||||
try {
|
||||
const clientQuery = `
|
||||
SELECT ClienteAutoridad, Nombre, Usuario, BD_Shelter
|
||||
FROM [CONTROLDESK].[dbo].[Usuarios]
|
||||
WHERE Usuario = @nodoName
|
||||
`;
|
||||
const req = secondary.request();
|
||||
req.input('nodoName', nodoName);
|
||||
const res = await req.query(clientQuery);
|
||||
clientData = res.recordset[0];
|
||||
} catch {}
|
||||
}
|
||||
|
||||
backupFiles.push({
|
||||
name: file,
|
||||
nodo_name: nodoName,
|
||||
client_name: clientData?.Nombre ?? 'Cliente no identificado',
|
||||
client_authority: clientData?.ClienteAutoridad ?? 'N/A',
|
||||
bd_shelter: clientData?.BD_Shelter ?? 'N/A',
|
||||
date: stats.mtime,
|
||||
size: (stats.size / 1024 / 1024).toFixed(2) + " MB"
|
||||
});
|
||||
}
|
||||
|
||||
// Hydrate Alerts if possible
|
||||
if (secondary && alertsData.length > 0) {
|
||||
const hydratedAlerts = [];
|
||||
for (const alert of alertsData) {
|
||||
const nodoName = alert.visible_name;
|
||||
let cData: any = null;
|
||||
try {
|
||||
const req = secondary.request();
|
||||
req.input('nodoName', nodoName);
|
||||
const res = await req.query(`
|
||||
SELECT u.ClienteAutoridad, u.Nombre, u.Usuario, bd.CorreoNotificacion
|
||||
FROM [CONTROLDESK].[dbo].[Usuarios] AS u
|
||||
LEFT JOIN [CONTROLDESK].[dbo].[BasesDeDatos] AS bd ON u.Usuario = bd.NodoSubNodo
|
||||
WHERE u.Usuario = @nodoName
|
||||
`);
|
||||
cData = res.recordset[0];
|
||||
} catch {}
|
||||
|
||||
hydratedAlerts.push({ ...alert, clientData: cData });
|
||||
}
|
||||
alertsData = hydratedAlerts;
|
||||
}
|
||||
|
||||
} catch (e: any) {
|
||||
console.error("Error processing backups/alerts hydration:", e);
|
||||
}
|
||||
|
||||
return {
|
||||
databaseRows,
|
||||
summaryMain,
|
||||
restoredCount,
|
||||
notRestoredCount,
|
||||
|
||||
databaseRowsAZ,
|
||||
summaryAZ,
|
||||
|
||||
backupFiles,
|
||||
clientsData,
|
||||
alertsData,
|
||||
basesDeDatosList,
|
||||
|
||||
errors // Return the collected errors
|
||||
};
|
||||
};
|
||||
|
||||
346
src/routes/+page.svelte
Normal file
346
src/routes/+page.svelte
Normal 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>
|
||||
16
svelte.config.js
Normal file
16
svelte.config.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import adapter from '@sveltejs/adapter-node';
|
||||
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
||||
|
||||
/** @type {import('@sveltejs/kit').Config} */
|
||||
const config = {
|
||||
// Consult https://kit.svelte.dev/docs/integrations#preprocessors
|
||||
// for more information about preprocessors
|
||||
preprocess: vitePreprocess(),
|
||||
|
||||
kit: {
|
||||
// adapter-node creates a Node.js server for the build
|
||||
adapter: adapter()
|
||||
}
|
||||
};
|
||||
|
||||
export default config;
|
||||
18
tsconfig.json
Normal file
18
tsconfig.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"extends": "./.svelte-kit/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
"sourceMap": true,
|
||||
"strict": true,
|
||||
"moduleResolution": "bundler"
|
||||
}
|
||||
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
|
||||
//
|
||||
// If you want to overwrite includes/excludes, make sure to copy them from .svelte-kit/tsconfig.json
|
||||
// "include": []
|
||||
}
|
||||
6
vite.config.ts
Normal file
6
vite.config.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { sveltekit } from '@sveltejs/kit/vite';
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [sveltekit()]
|
||||
});
|
||||
Reference in New Issue
Block a user