AutoBackups/templates/dashboard.html

1284 lines
52 KiB
HTML

{% extends "base.html" %}
{% block title %}Dashboard - AutoBackups{% endblock %}
{% block content %}
<div class="row">
<!-- Statistics Cards -->
<div class="col-12">
<div class="row mb-4">
<div class="col-md-3">
<div class="card text-center bg-primary text-white">
<div class="card-body">
<h5 class="card-title">
<i class="bi bi-folder2-open"></i> Total Proyectos
</h5>
<h2 class="card-text">{{ stats.total_projects }}</h2>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card text-center bg-success text-white">
<div class="card-body">
<h5 class="card-title">
<i class="bi bi-check-circle"></i> Habilitados
</h5>
<h2 class="card-text">{{ stats.enabled_projects }}</h2>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card text-center bg-info text-white">
<div class="card-body">
<h5 class="card-title">
<i class="bi bi-hdd"></i> Espacio Libre
</h5>
<h2 class="card-text">{{ "%.1f"|format(stats.free_space_mb) }} MB</h2>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card text-center bg-warning text-white">
<div class="card-body">
<h5 class="card-title">
<i class="bi bi-hdd-stack"></i> Espacio Total
</h5>
<h2 class="card-text">{{ "%.1f"|format(stats.total_space_mb) }} MB</h2>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<!-- Projects Table -->
<div class="col-12">
<div class="card">
<div class="card-header">
<div class="d-flex justify-content-between align-items-center mb-3">
<h5 class="mb-0">
<i class="bi bi-list-task"></i> Proyectos
</h5>
<div>
<button class="btn btn-outline-primary btn-sm" id="refresh-projects-btn">
<i class="bi bi-arrow-clockwise"></i> Actualizar
</button>
<button class="btn btn-success btn-sm" id="backup-all-btn">
<i class="bi bi-archive"></i> Backup Todos
</button>
</div>
</div>
<!-- Search and View Controls -->
<div class="row">
<div class="col-md-6">
<div class="input-group">
<span class="input-group-text">
<i class="bi bi-search"></i>
</span>
<input type="text" class="form-control" id="project-search"
placeholder="Buscar proyectos por nombre o ruta...">
</div>
</div>
<div class="col-md-6">
<div class="d-flex gap-2">
<select class="form-select" id="view-mode-select">
<option value="hierarchical">Vista Jerárquica</option>
<option value="flat">Vista Plana</option>
</select>
<select class="form-select" id="filter-status">
<option value="all">Todos los estados</option>
<option value="enabled">Solo habilitados</option>
<option value="ready">Listos</option>
<option value="error">Con errores</option>
</select>
<button class="btn btn-outline-secondary" id="expand-collapse-all-btn" title="Expandir/Colapsar Todo">
<i class="bi bi-arrows-expand" id="expand-collapse-icon"></i>
</button>
</div>
</div>
</div>
</div>
<div class="card-body">
{% if projects %}
<!-- Projects Container -->
<div id="projects-container">
<!-- Hierarchical View -->
<div id="hierarchical-view" class="projects-view">
<div id="hierarchical-projects"></div>
</div>
<!-- Flat View (Table) -->
<div id="flat-view" class="projects-view" style="display: none;">
<div class="table-responsive">
<table class="table table-striped table-hover" id="projects-table">
<thead class="table-dark">
<tr>
<th>Proyecto</th>
<th>Tipo</th>
<th>Estado</th>
<th>Último Backup</th>
<th>Próximo Backup</th>
<th>Acciones</th>
</tr>
</thead>
<tbody>
{% for project in projects %}
<tr class="project-row"
data-project-name="{{ project.name or 'Sin nombre' }}"
data-project-path="{{ project.path }}"
data-project-status="{{ project.status.current_status }}"
data-project-enabled="{{ project.schedule_config.enabled }}">
<td>
<strong>{{ project.name or 'Sin nombre' }}</strong>
<br>
<small class="text-muted">{{ project.path }}</small>
</td>
<td>
<span class="badge bg-secondary">{{ project.type or 'N/A' }}</span>
</td>
<td>
{% if project.status.current_status == 'ready' %}
<span class="badge bg-success">Listo</span>
{% elif project.status.current_status == 'backing_up' %}
<span class="badge bg-warning">En progreso</span>
{% elif project.status.current_status == 'error' %}
<span class="badge bg-danger">Error</span>
{% elif project.status.current_status == 'files_in_use' %}
<span class="badge bg-warning">Archivos en uso</span>
{% else %}
<span class="badge bg-secondary">{{ project.status.current_status }}</span>
{% endif %}
</td>
<td>
{% if project.backup_history.last_backup_date %}
{{ project.backup_history.last_backup_date[:19] }}
{% else %}
<span class="text-muted">Nunca</span>
{% endif %}
</td>
<td>
{% if project.schedule_config.next_scheduled_backup %}
{{ project.schedule_config.next_scheduled_backup[:19] }}
{% else %}
<span class="text-muted">No programado</span>
{% endif %}
</td>
<td>
<div class="btn-group btn-group-sm" role="group">
<button class="btn btn-outline-primary backup-project-btn"
data-project-id="{{ project.id }}">
<i class="bi bi-archive"></i>
</button>
<button class="btn btn-outline-secondary config-project-btn"
data-project-id="{{ project.id }}">
<i class="bi bi-gear"></i>
</button>
<button class="btn btn-outline-info view-backups-btn"
data-project-id="{{ project.id }}"
data-project-name="{{ project.name or 'Sin nombre' }}"
data-project-path="{{ project.path }}">
<i class="bi bi-eye"></i>
</button>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% else %}
<div class="text-center py-5">
<i class="bi bi-folder-x display-1 text-muted"></i>
<h4 class="text-muted mt-3">No hay proyectos disponibles</h4>
<p class="text-muted">Ejecuta un escaneo para buscar proyectos en los directorios configurados.</p>
<button class="btn btn-primary" id="first-scan-btn">
<i class="bi bi-search"></i> Escanear Proyectos
</button>
</div>
{% endif %}
</div>
</div>
</div>
</div>
{% endblock %}
{% block modals %}
<!-- Project Configuration Modal -->
<div class="modal fade" id="projectConfigModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Configuración del Proyecto</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="projectConfigForm">
<input type="hidden" id="modal-project-id">
<div class="mb-3">
<label for="modal-project-schedule" class="form-label">Frecuencia de Backup</label>
<select class="form-select" id="modal-project-schedule">
<option value="manual">Manual</option>
<option value="daily">Diario</option>
<option value="hourly">Cada hora</option>
<option value="3-hour">Cada 3 horas</option>
<option value="7-hour">Cada 7 horas</option>
<option value="startup">Al iniciar</option>
</select>
</div>
<div class="mb-3">
<label for="modal-project-time" class="form-label">Hora de Backup (para programación diaria)</label>
<input type="time" class="form-control" id="modal-project-time">
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="modal-project-enabled">
<label class="form-check-label" for="modal-project-enabled">
Backup habilitado
</label>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
<button type="button" class="btn btn-primary" id="save-project-config-btn">Guardar</button>
</div>
</div>
</div>
</div>
<!-- System Status Modal -->
<div class="modal fade" id="systemStatusModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Estado del Sistema</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body" id="system-status-content">
<div class="text-center">
<div class="spinner-border" role="status">
<span class="visually-hidden">Cargando...</span>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
document.addEventListener('DOMContentLoaded', function() {
// Event listeners for project actions
document.querySelectorAll('.backup-project-btn').forEach(btn => {
btn.addEventListener('click', function() {
const projectId = this.dataset.projectId;
backupProject(projectId);
});
});
document.querySelectorAll('.config-project-btn').forEach(btn => {
btn.addEventListener('click', function() {
const projectId = this.dataset.projectId;
openProjectConfig(projectId);
});
});
// Other event listeners
const refreshBtn = document.getElementById('refresh-projects-btn');
if (refreshBtn) {
refreshBtn.addEventListener('click', () => location.reload());
}
const scanBtn = document.getElementById('scan-projects-btn');
if (scanBtn) {
scanBtn.addEventListener('click', scanProjects);
}
const firstScanBtn = document.getElementById('first-scan-btn');
if (firstScanBtn) {
firstScanBtn.addEventListener('click', scanProjects);
}
const systemStatusBtn = document.getElementById('system-status-btn');
if (systemStatusBtn) {
systemStatusBtn.addEventListener('click', showSystemStatus);
}
});
function backupProject(projectId) {
showAlert('info', `Iniciando backup del proyecto ${projectId}...`);
fetch(`/api/projects/${projectId}/backup`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
}
})
.then(response => response.json())
.then(data => {
if (data.error) {
showAlert('danger', `Error: ${data.error}`);
} else {
showAlert('success', data.message || 'Backup iniciado correctamente');
}
})
.catch(error => {
showAlert('danger', `Error de conexión: ${error.message}`);
});
}
function openProjectConfig(projectId) {
document.getElementById('modal-project-id').value = projectId;
const modal = new bootstrap.Modal(document.getElementById('projectConfigModal'));
modal.show();
}
function scanProjects() {
showAlert('info', 'Escaneando directorios en busca de proyectos...');
fetch('/api/scan', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
}
})
.then(response => response.json())
.then(data => {
if (data.error) {
showAlert('danger', `Error: ${data.error}`);
} else {
showAlert('success', `Escaneo completado. ${data.projects_found} proyectos encontrados.`);
setTimeout(() => location.reload(), 2000);
}
})
.catch(error => {
showAlert('danger', `Error de conexión: ${error.message}`);
});
}
function showSystemStatus() {
const modal = new bootstrap.Modal(document.getElementById('systemStatusModal'));
fetch('/api/system/status')
.then(response => response.json())
.then(data => {
if (data.error) {
document.getElementById('system-status-content').innerHTML =
`<div class="alert alert-danger">Error: ${data.error}</div>`;
} else {
document.getElementById('system-status-content').innerHTML = `
<div class="row">
<div class="col-md-6">
<h6>Espacio en Disco</h6>
<ul class="list-group list-group-flush">
<li class="list-group-item d-flex justify-content-between">
<span>Total:</span>
<span>${data.disk_space.total_mb.toFixed(1)} MB</span>
</li>
<li class="list-group-item d-flex justify-content-between">
<span>Usado:</span>
<span>${data.disk_space.used_mb.toFixed(1)} MB</span>
</li>
<li class="list-group-item d-flex justify-content-between">
<span>Libre:</span>
<span>${data.disk_space.free_mb.toFixed(1)} MB</span>
</li>
</ul>
</div>
<div class="col-md-6">
<h6>Proyectos</h6>
<ul class="list-group list-group-flush">
<li class="list-group-item d-flex justify-content-between">
<span>Total:</span>
<span>${data.projects.total}</span>
</li>
<li class="list-group-item d-flex justify-content-between">
<span>Habilitados:</span>
<span>${data.projects.enabled}</span>
</li>
<li class="list-group-item d-flex justify-content-between">
<span>Everything API:</span>
<span class="badge ${data.everything_api.available ? 'bg-success' : 'bg-warning'}">
${data.everything_api.available ? 'Disponible' : 'No disponible'}
</span>
</li>
</ul>
</div>
</div>
`;
}
})
.catch(error => {
document.getElementById('system-status-content').innerHTML =
`<div class="alert alert-danger">Error de conexión: ${error.message}</div>`;
});
modal.show();
}
// Project search and filtering functionality
// Re-enable the projects data now that we fixed the serialization
const projectsData = {{ projects | tojson | safe }};
console.log('Dashboard loaded, projects data:', projectsData);
function initializeProjectsView() {
const searchInput = document.getElementById('project-search');
const viewModeSelect = document.getElementById('view-mode-select');
const filterStatusSelect = document.getElementById('filter-status');
if (searchInput) {
searchInput.addEventListener('input', function() {
filterAndDisplayProjects();
});
}
if (viewModeSelect) {
viewModeSelect.addEventListener('change', function() {
toggleViewMode(this.value);
filterAndDisplayProjects();
});
}
if (filterStatusSelect) {
filterStatusSelect.addEventListener('change', function() {
filterAndDisplayProjects();
});
}
// Initialize hierarchical view
if (projectsData && projectsData.length > 0) {
buildHierarchicalView();
// Set default view mode to hierarchical
toggleViewMode('hierarchical');
filterAndDisplayProjects();
} else {
console.log('No projects data available for hierarchical view');
}
}
function toggleViewMode(mode) {
const hierarchicalView = document.getElementById('hierarchical-view');
const flatView = document.getElementById('flat-view');
if (mode === 'hierarchical') {
hierarchicalView.style.display = 'block';
flatView.style.display = 'none';
} else {
hierarchicalView.style.display = 'none';
flatView.style.display = 'block';
}
}
function filterAndDisplayProjects() {
const searchTerm = document.getElementById('project-search').value.toLowerCase();
const statusFilter = document.getElementById('filter-status').value;
const viewMode = document.getElementById('view-mode-select').value;
let filteredProjects = projectsData.filter(project => {
// Search filter
const matchesSearch = !searchTerm ||
(project.name && project.name.toLowerCase().includes(searchTerm)) ||
(project.path && project.path.toLowerCase().includes(searchTerm));
// Status filter
let matchesStatus = true;
if (statusFilter === 'enabled') {
matchesStatus = project.schedule_config && project.schedule_config.enabled;
} else if (statusFilter === 'ready') {
const currentStatus = (project.status && project.status.current_status) || project.current_status;
matchesStatus = currentStatus === 'ready';
} else if (statusFilter === 'error') {
const currentStatus = (project.status && project.status.current_status) || project.current_status;
matchesStatus = currentStatus === 'error';
}
return matchesSearch && matchesStatus;
});
if (viewMode === 'hierarchical') {
displayHierarchicalProjects(filteredProjects);
} else {
displayFlatProjects(filteredProjects);
}
}
function buildHierarchicalView() {
console.log('Building optimized hierarchical view with', projectsData.length, 'projects');
// Get observation directories from config to use as roots
const observationDirs = getObservationDirectories();
const hierarchicalStructure = {};
// Group projects by observation directory
observationDirs.forEach(obsDir => {
hierarchicalStructure[obsDir.path] = {
type: 'observation_root',
displayName: obsDir.description || getLastDirectoryName(obsDir.path),
fullPath: obsDir.path,
projects: [],
subdirectories: {},
enabled: obsDir.enabled
};
});
// Process each project
projectsData.forEach(project => {
const projectPath = project.path || '';
// Find which observation directory this project belongs to
const belongsToObsDir = observationDirs.find(obsDir =>
projectPath.toLowerCase().startsWith(obsDir.path.toLowerCase())
);
if (!belongsToObsDir) {
console.log('Project does not belong to any observation directory:', project.name);
return;
}
const obsDirKey = belongsToObsDir.path;
const relativePath = projectPath.substring(belongsToObsDir.path.length);
const pathParts = relativePath.split(/[\\\/]/).filter(part => part !== '');
if (pathParts.length === 0) {
// Project is directly in observation directory
hierarchicalStructure[obsDirKey].projects.push(project);
return;
}
// Build optimized path structure
addProjectToOptimizedStructure(
hierarchicalStructure[obsDirKey].subdirectories,
pathParts,
project,
belongsToObsDir.path
);
});
// Optimize the structure by collapsing linear paths
Object.keys(hierarchicalStructure).forEach(obsDirKey => {
hierarchicalStructure[obsDirKey].subdirectories =
optimizeDirectoryStructure(hierarchicalStructure[obsDirKey].subdirectories);
});
console.log('Optimized hierarchical structure built:', hierarchicalStructure);
window.hierarchicalStructure = hierarchicalStructure;
}
function addProjectToOptimizedStructure(currentLevel, pathParts, project, basePath) {
if (pathParts.length === 0) return;
const currentDir = pathParts[0];
const remainingParts = pathParts.slice(1);
if (!currentLevel[currentDir]) {
currentLevel[currentDir] = {
type: 'directory',
displayName: currentDir,
fullPath: basePath + '\\' + pathParts.slice(0, 1).join('\\'),
projects: [],
subdirectories: {},
pathParts: [currentDir]
};
}
if (remainingParts.length === 0) {
// This is the final directory containing the project
currentLevel[currentDir].projects.push(project);
} else {
// Continue building the structure
addProjectToOptimizedStructure(
currentLevel[currentDir].subdirectories,
remainingParts,
project,
basePath + '\\' + pathParts.slice(0, 1).join('\\')
);
// Update the path parts for this directory
currentLevel[currentDir].pathParts = pathParts.slice(0, pathParts.length - remainingParts.length);
}
}
function optimizeDirectoryStructure(structure) {
const optimized = {};
Object.keys(structure).forEach(dirName => {
const dirData = structure[dirName];
const subdirKeys = Object.keys(dirData.subdirectories);
// If this directory has no projects and only one subdirectory, consider collapsing
if (dirData.projects.length === 0 && subdirKeys.length === 1) {
const singleSubdir = dirData.subdirectories[subdirKeys[0]];
// Check if we can collapse this path segment
if (shouldCollapsePath(dirData, singleSubdir)) {
// Create collapsed path display
const collapsedName = `${dirData.displayName}\\${singleSubdir.displayName}`;
const collapsedDir = {
...singleSubdir,
displayName: collapsedName,
type: 'collapsed_path',
originalPath: [dirData.displayName, singleSubdir.displayName],
subdirectories: optimizeDirectoryStructure(singleSubdir.subdirectories)
};
optimized[`${dirName}_${subdirKeys[0]}`] = collapsedDir;
} else {
// Keep as separate directory but optimize its subdirectories
optimized[dirName] = {
...dirData,
subdirectories: optimizeDirectoryStructure(dirData.subdirectories)
};
}
} else {
// Keep as is but optimize subdirectories
optimized[dirName] = {
...dirData,
subdirectories: optimizeDirectoryStructure(dirData.subdirectories)
};
}
});
return optimized;
}
function shouldCollapsePath(parentDir, childDir) {
// Collapse if:
// 1. Parent has no projects
// 2. Child has projects or multiple subdirectories (makes sense to show the path)
// 3. The combined path length is reasonable
const combinedName = `${parentDir.displayName}\\${childDir.displayName}`;
return parentDir.projects.length === 0 &&
combinedName.length < 50 &&
(childDir.projects.length > 0 || Object.keys(childDir.subdirectories).length > 1);
}
function getObservationDirectories() {
// This should come from the server, but for now we'll extract from project paths
// In a real implementation, you'd pass this from the Flask route
const obsDirs = new Set();
projectsData.forEach(project => {
const path = project.path || '';
// Extract likely observation directory (first 2-3 path segments)
const parts = path.split(/[\\\/]/).filter(p => p !== '');
if (parts.length >= 2) {
// Assume observation directory is the first 2 levels (e.g., "C:\Trabajo\SIDEL")
const obsDir = parts.slice(0, 3).join('\\');
obsDirs.add(obsDir);
}
});
return Array.from(obsDirs).map(path => ({
path: path,
description: getLastDirectoryName(path),
enabled: true
}));
}
function getLastDirectoryName(path) {
const parts = path.split(/[\\\/]/).filter(p => p !== '');
return parts[parts.length - 1] || path;
}
function displayHierarchicalProjects(filteredProjects) {
const container = document.getElementById('hierarchical-projects');
if (!container) return;
// Filter the hierarchical structure
const filteredStructure = filterHierarchicalStructure(window.hierarchicalStructure, filteredProjects);
container.innerHTML = renderHierarchicalStructure(filteredStructure);
// Bind events to hierarchical elements
bindHierarchicalEvents();
}
function filterHierarchicalStructure(structure, filteredProjects) {
const filtered = {};
Object.keys(structure).forEach(key => {
const dirData = structure[key];
let filteredDir;
if (dirData.type === 'observation_root') {
filteredDir = {
...dirData,
projects: [],
subdirectories: {}
};
} else {
filteredDir = {
...dirData,
projects: [],
subdirectories: {}
};
}
// Filter projects in this directory
filteredDir.projects = dirData.projects.filter(project =>
filteredProjects.some(fp => fp.id === project.id)
);
// Recursively filter subdirectories
filteredDir.subdirectories = filterHierarchicalStructure(
dirData.subdirectories,
filteredProjects
);
// Include directory if it has projects or subdirectories
if (filteredDir.projects.length > 0 ||
Object.keys(filteredDir.subdirectories).length > 0) {
filtered[key] = filteredDir;
}
});
return filtered;
}
function renderHierarchicalStructure(structure, level = 0) {
let html = '';
Object.keys(structure).forEach(key => {
const dirData = structure[key];
const hasProjects = dirData.projects.length > 0;
const hasSubdirs = Object.keys(dirData.subdirectories).length > 0;
// Different rendering based on directory type
if (dirData.type === 'observation_root') {
html += renderObservationDirectory(dirData, level);
} else if (dirData.type === 'collapsed_path') {
html += renderCollapsedPath(dirData, level);
} else {
html += renderRegularDirectory(dirData, level);
}
});
return html;
}
function renderObservationDirectory(dirData, level) {
const hasContent = dirData.projects.length > 0 || Object.keys(dirData.subdirectories).length > 0;
const enabledClass = dirData.enabled ? '' : 'opacity-50';
let html = `
<div class="observation-directory ${enabledClass}" style="margin-left: ${level * 15}px;">
<div class="directory-header observation-header">
${hasContent ?
`<button class="btn btn-sm btn-link p-0 directory-toggle" type="button">
<i class="bi bi-chevron-down"></i>
</button>` : ''
}
<i class="bi bi-hdd-fill text-primary me-2"></i>
<strong class="text-primary">${dirData.displayName}</strong>
<small class="text-muted ms-2">${dirData.fullPath}</small>
${dirData.projects.length > 0 ?
`<span class="badge bg-primary ms-2">${dirData.projects.length}</span>` : ''
}
</div>
<div class="directory-content" style="display: block;">
`;
// Add projects directly in this observation directory
dirData.projects.forEach(project => {
html += renderProjectCard(project, level + 1);
});
// Add subdirectories
html += renderHierarchicalStructure(dirData.subdirectories, level + 1);
html += `
</div>
</div>
`;
return html;
}
function renderCollapsedPath(dirData, level) {
const hasProjects = dirData.projects.length > 0;
const hasSubdirs = Object.keys(dirData.subdirectories).length > 0;
let html = `
<div class="collapsed-path-directory" style="margin-left: ${level * 15}px;">
<div class="directory-header collapsed-header">
${hasSubdirs || hasProjects ?
`<button class="btn btn-sm btn-link p-0 directory-toggle" type="button">
<i class="bi bi-chevron-down"></i>
</button>` : ''
}
<i class="bi bi-folder-symlink text-info me-2"></i>
<span class="text-info">${dirData.displayName}</span>
${hasProjects ? `<span class="badge bg-info ms-2">${dirData.projects.length}</span>` : ''}
</div>
<div class="directory-content" style="display: block;">
`;
// Add projects in this collapsed path
dirData.projects.forEach(project => {
html += renderProjectCard(project, level + 1);
});
// Add subdirectories
html += renderHierarchicalStructure(dirData.subdirectories, level + 1);
html += `
</div>
</div>
`;
return html;
}
function renderRegularDirectory(dirData, level) {
const hasProjects = dirData.projects.length > 0;
const hasSubdirs = Object.keys(dirData.subdirectories).length > 0;
let html = `
<div class="regular-directory" style="margin-left: ${level * 15}px;">
<div class="directory-header">
${hasSubdirs || hasProjects ?
`<button class="btn btn-sm btn-link p-0 directory-toggle" type="button">
<i class="bi bi-chevron-down"></i>
</button>` : ''
}
<i class="bi bi-folder-fill text-warning me-2"></i>
<strong>${dirData.displayName}</strong>
${hasProjects ? `<span class="badge bg-secondary ms-2">${dirData.projects.length}</span>` : ''}
</div>
<div class="directory-content" style="display: block;">
`;
// Add projects in this directory
dirData.projects.forEach(project => {
html += renderProjectCard(project, level + 1);
});
// Add subdirectories
html += renderHierarchicalStructure(dirData.subdirectories, level + 1);
html += `
</div>
</div>
`;
return html;
}
function renderProjectCard(project, level) {
const statusBadge = getStatusBadge(project.status || project.current_status);
const projectName = project.name || 'Sin nombre';
const projectType = project.type || 'N/A';
const projectId = project.id || '';
const projectPath = project.path || '';
return `
<div class="project-card card mb-2" style="margin-left: ${level * 20}px;">
<div class="card-body p-2">
<div class="row align-items-center">
<div class="col-md-6">
<h6 class="card-title mb-1">
<i class="bi bi-file-earmark-code text-primary me-2"></i>
${projectName}
</h6>
<small class="text-muted">
<i class="bi bi-folder me-1"></i>
${projectPath}
</small>
</div>
<div class="col-md-2">
<span class="badge bg-secondary">${projectType}</span>
</div>
<div class="col-md-2">
${statusBadge}
</div>
<div class="col-md-2 text-end">
<div class="btn-group btn-group-sm" role="group">
<button class="btn btn-outline-primary backup-project-btn"
data-project-id="${projectId}"
title="Hacer backup">
<i class="bi bi-archive"></i>
</button>
<button class="btn btn-outline-secondary config-project-btn"
data-project-id="${projectId}"
title="Configurar">
<i class="bi bi-gear"></i>
</button>
<button class="btn btn-outline-info view-backups-btn"
data-project-id="${projectId}"
data-project-name="${projectName}"
data-project-path="${projectPath}"
title="Ver backups y carpetas">
<i class="bi bi-eye"></i>
</button>
</div>
</div>
</div>
</div>
</div>
`;
}
function getStatusBadge(status) {
// Handle both object and string status values
let statusValue = status;
if (typeof status === 'object') {
statusValue = status.current_status || status.value || status;
}
const statusMap = {
'ready': '<span class="badge bg-success">Listo</span>',
'backing_up': '<span class="badge bg-warning">En progreso</span>',
'error': '<span class="badge bg-danger">Error</span>',
'files_in_use': '<span class="badge bg-warning">Archivos en uso</span>'
};
return statusMap[statusValue] || `<span class="badge bg-secondary">${statusValue}</span>`;
}
function displayFlatProjects(filteredProjects) {
const tableBody = document.querySelector('#projects-table tbody');
if (!tableBody) return;
const rows = tableBody.querySelectorAll('.project-row');
rows.forEach(row => {
const projectName = row.dataset.projectName.toLowerCase();
const projectPath = row.dataset.projectPath.toLowerCase();
const projectStatus = row.dataset.projectStatus;
const projectEnabled = row.dataset.projectEnabled === 'True' || row.dataset.projectEnabled === 'true';
// Get current filters
const searchTerm = document.getElementById('project-search').value.toLowerCase();
const statusFilter = document.getElementById('filter-status').value;
// Check if row matches filters
const matchesSearch = !searchTerm || projectName.includes(searchTerm) || projectPath.includes(searchTerm);
let matchesStatus = true;
if (statusFilter === 'enabled') {
matchesStatus = projectEnabled;
} else if (statusFilter === 'ready') {
matchesStatus = projectStatus === 'ready';
} else if (statusFilter === 'error') {
matchesStatus = projectStatus === 'error';
}
row.style.display = (matchesSearch && matchesStatus) ? '' : 'none';
});
// Bind events for flat view buttons
bindFlatViewEvents();
}
function bindFlatViewEvents() {
// Bind backup buttons
document.querySelectorAll('.backup-project-btn').forEach(btn => {
btn.replaceWith(btn.cloneNode(true)); // Remove existing listeners
});
document.querySelectorAll('.backup-project-btn').forEach(btn => {
btn.addEventListener('click', function() {
const projectId = this.dataset.projectId;
backupProject(projectId);
});
});
// Bind config buttons
document.querySelectorAll('.config-project-btn').forEach(btn => {
btn.replaceWith(btn.cloneNode(true)); // Remove existing listeners
});
document.querySelectorAll('.config-project-btn').forEach(btn => {
btn.addEventListener('click', function() {
const projectId = this.dataset.projectId;
openProjectConfig(projectId);
});
});
// Bind view backups buttons
document.querySelectorAll('.view-backups-btn').forEach(btn => {
btn.replaceWith(btn.cloneNode(true)); // Remove existing listeners
});
document.querySelectorAll('.view-backups-btn').forEach(btn => {
btn.addEventListener('click', function() {
const projectId = this.dataset.projectId;
const projectName = this.dataset.projectName;
const projectPath = this.dataset.projectPath;
showBackupsModal(projectId, projectName, projectPath);
});
});
}
function bindHierarchicalEvents() {
// Directory toggle functionality
document.querySelectorAll('.directory-toggle').forEach(toggle => {
toggle.addEventListener('click', function() {
// Find the content div - it should be a sibling of the directory header
const header = this.closest('.directory-header');
const parentDiv = header.parentElement;
const content = parentDiv.querySelector('.directory-content');
const icon = this.querySelector('i');
if (content && content.style.display === 'block') {
content.style.display = 'none';
icon.className = 'bi bi-chevron-right';
} else if (content) {
content.style.display = 'block';
icon.className = 'bi bi-chevron-down';
}
});
});
// Re-bind project action buttons
document.querySelectorAll('.backup-project-btn').forEach(btn => {
btn.addEventListener('click', function() {
const projectId = this.dataset.projectId;
backupProject(projectId);
});
});
document.querySelectorAll('.config-project-btn').forEach(btn => {
btn.addEventListener('click', function() {
const projectId = this.dataset.projectId;
openProjectConfig(projectId);
});
});
// Re-bind view backups buttons
document.querySelectorAll('.view-backups-btn').forEach(btn => {
btn.addEventListener('click', function() {
const projectId = this.dataset.projectId;
const projectName = this.dataset.projectName;
const projectPath = this.dataset.projectPath;
showBackupsModal(projectId, projectName, projectPath);
});
});
}
// Global expand/collapse functionality
function initializeExpandCollapseButton() {
const expandCollapseBtn = document.getElementById('expand-collapse-all-btn');
const expandCollapseIcon = document.getElementById('expand-collapse-icon');
let isAllExpanded = true; // Start as expanded by default
if (expandCollapseBtn) {
expandCollapseBtn.addEventListener('click', function() {
const contents = document.querySelectorAll('.directory-content');
const toggles = document.querySelectorAll('.directory-toggle i');
if (isAllExpanded) {
// Collapse all
contents.forEach(content => {
content.style.display = 'none';
});
toggles.forEach(icon => {
icon.className = 'bi bi-chevron-right';
});
expandCollapseIcon.className = 'bi bi-arrows-collapse';
expandCollapseBtn.title = 'Expandir Todo';
isAllExpanded = false;
} else {
// Expand all
contents.forEach(content => {
content.style.display = 'block';
});
toggles.forEach(icon => {
icon.className = 'bi bi-chevron-down';
});
expandCollapseIcon.className = 'bi bi-arrows-expand';
expandCollapseBtn.title = 'Colapsar Todo';
isAllExpanded = true;
}
});
}
}
// Show backups modal with backup list and folder actions
function showBackupsModal(projectId, projectName, projectPath) {
// Create modal if it doesn't exist
let modal = document.getElementById('backupsModal');
if (!modal) {
modal = createBackupsModal();
document.body.appendChild(modal);
}
// Update modal content
document.getElementById('backupsModalTitle').textContent = `Backups de ${projectName}`;
document.getElementById('backupsProjectPath').textContent = projectPath;
// Set up folder buttons
const openSourceBtn = document.getElementById('openSourceFolderBtn');
const openBackupBtn = document.getElementById('openBackupFolderBtn');
openSourceBtn.onclick = () => openProjectFolder(projectId, 'source');
openBackupBtn.onclick = () => openProjectFolder(projectId, 'backup');
// Load backup list
loadBackupList(projectId);
// Show modal
const bsModal = new bootstrap.Modal(modal);
bsModal.show();
}
function createBackupsModal() {
const modalHTML = `
<div class="modal fade" id="backupsModal" tabindex="-1" aria-labelledby="backupsModalTitle" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="backupsModalTitle">Backups del Proyecto</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<strong>Ruta del proyecto:</strong>
<p class="text-muted mb-2" id="backupsProjectPath"></p>
<div class="btn-group" role="group">
<button type="button" class="btn btn-outline-primary btn-sm" id="openSourceFolderBtn">
<i class="bi bi-folder-fill me-1"></i>Abrir Carpeta Origen
</button>
<button type="button" class="btn btn-outline-info btn-sm" id="openBackupFolderBtn">
<i class="bi bi-archive-fill me-1"></i>Abrir Carpeta Backups
</button>
</div>
</div>
<hr>
<div id="backupListContainer">
<div class="text-center">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Cargando...</span>
</div>
<p class="mt-2">Cargando lista de backups...</p>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cerrar</button>
</div>
</div>
</div>
</div>
`;
const tempDiv = document.createElement('div');
tempDiv.innerHTML = modalHTML;
return tempDiv.firstElementChild;
}
function loadBackupList(projectId) {
const container = document.getElementById('backupListContainer');
fetch(`/api/projects/${projectId}/backups`)
.then(response => response.json())
.then(data => {
if (data.error) {
container.innerHTML = `
<div class="alert alert-danger">
<i class="bi bi-exclamation-triangle me-2"></i>
Error: ${data.error}
</div>
`;
return;
}
if (data.backup_count === 0) {
container.innerHTML = `
<div class="alert alert-info">
<i class="bi bi-info-circle me-2"></i>
No se encontraron backups para este proyecto.
</div>
`;
return;
}
// Build backup list
let html = `
<h6>Backups encontrados (${data.backup_count})</h6>
<div class="table-responsive">
<table class="table table-sm table-striped">
<thead class="table-dark">
<tr>
<th>Fecha</th>
<th>Archivo</th>
<th>Tamaño</th>
<th>Creado</th>
</tr>
</thead>
<tbody>
`;
data.backups.forEach(backup => {
const createdDate = new Date(backup.created).toLocaleString('es-ES');
html += `
<tr>
<td>${backup.date}</td>
<td>
<i class="bi bi-file-zip me-1"></i>
${backup.filename}
</td>
<td>${backup.size_mb} MB</td>
<td>${createdDate}</td>
</tr>
`;
});
html += `
</tbody>
</table>
</div>
<small class="text-muted">
<i class="bi bi-folder me-1"></i>
Ubicación: ${data.backup_destination}
</small>
`;
container.innerHTML = html;
})
.catch(error => {
console.error('Error loading backup list:', error);
container.innerHTML = `
<div class="alert alert-danger">
<i class="bi bi-exclamation-triangle me-2"></i>
Error cargando la lista de backups: ${error.message}
</div>
`;
});
}
function openProjectFolder(projectId, folderType) {
fetch(`/api/projects/${projectId}/open-folder`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ type: folderType })
})
.then(response => response.json())
.then(data => {
if (data.error) {
showNotification(`Error: ${data.error}`, 'error');
} else {
const folderName = folderType === 'source' ? 'origen' : 'backups';
showNotification(`Carpeta ${folderName} abierta en el explorador`, 'success');
}
})
.catch(error => {
console.error('Error opening folder:', error);
showNotification(`Error abriendo la carpeta: ${error.message}`, 'error');
});
}
// Initialize on page load
document.addEventListener('DOMContentLoaded', function() {
initializeProjectsView();
initializeExpandCollapseButton();
});
</script>
{% endblock %}