1062 lines
43 KiB
HTML
1062 lines
43 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>Ruta</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">ID: {{ project.id }}</small>
|
|
</td>
|
|
<td>
|
|
<span title="{{ project.path }}">
|
|
{{ project.path[:50] }}{% if project.path|length > 50 %}...{% endif %}
|
|
</span>
|
|
</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-project-btn"
|
|
data-project-id="{{ project.id }}">
|
|
<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-3">
|
|
<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">ID: ${projectId}</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}">
|
|
<i class="bi bi-archive"></i>
|
|
</button>
|
|
<button class="btn btn-outline-secondary config-project-btn"
|
|
data-project-id="${projectId}">
|
|
<i class="bi bi-gear"></i>
|
|
</button>
|
|
<button class="btn btn-outline-info view-project-btn"
|
|
data-project-id="${projectId}">
|
|
<i class="bi bi-eye"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="row mt-2">
|
|
<div class="col-12">
|
|
<small class="text-muted">
|
|
<i class="bi bi-folder me-1"></i>
|
|
${projectPath}
|
|
</small>
|
|
</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';
|
|
});
|
|
}
|
|
|
|
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);
|
|
});
|
|
});
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// Initialize on page load
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
initializeProjectsView();
|
|
initializeExpandCollapseButton();
|
|
});
|
|
</script>
|
|
{% endblock %}
|