| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174 |
- {% extends "base.html" %}
- {% block title %}Archives{% endblock %}
- {% block content %}
- <div class="flex items-center justify-between mb-6">
- <h1 class="text-xl font-bold text-gray-900">Archives</h1>
- <span class="text-sm text-gray-400">{{ items | length }} archive{{ 's' if items | length != 1 }}</span>
- </div>
- {% if not items %}
- <div class="bg-white rounded-xl border border-gray-200 px-6 py-12 text-center text-gray-400">
- <p class="text-lg">Aucune archive sur ce serveur.</p>
- <p class="text-sm mt-2">Les archives apparaissent ici après l'exécution d'un job de sauvegarde.</p>
- </div>
- {% else %}
- {# ── Filtres ─────────────────────────────────────────────────────── #}
- <div class="bg-white rounded-xl border border-gray-200 p-4 mb-4 flex flex-wrap gap-3 items-center">
- <input id="filter-search" type="text" placeholder="Filtrer par nom ou job…"
- oninput="applyFilters()"
- class="border border-gray-200 rounded px-3 py-1.5 text-sm flex-1 min-w-40 focus:outline-none focus:ring-2 focus:ring-blue-400">
- <select id="filter-status" onchange="applyFilters()"
- class="border border-gray-200 rounded px-3 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-blue-400">
- <option value="">Tous les statuts</option>
- <option value="success">✓ succès</option>
- <option value="error">✗ erreur</option>
- <option value="running">⟳ en cours</option>
- <option value="">— sans run</option>
- </select>
- <select id="filter-type" onchange="applyFilters()"
- class="border border-gray-200 rounded px-3 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-blue-400">
- <option value="">Tous les types</option>
- {% for t in items | map(attribute='type') | unique | sort %}
- {% if t %}<option value="{{ t }}">{{ t }}</option>{% endif %}
- {% endfor %}
- </select>
- </div>
- <div class="bg-white rounded-xl border border-gray-200 shadow-sm overflow-hidden">
- <div class="overflow-x-auto">
- <table class="w-full text-sm" id="archives-table">
- <thead>
- <tr class="text-xs text-gray-500 uppercase tracking-wide bg-gray-50">
- <th class="px-6 py-3 text-left font-medium">Archive</th>
- <th class="px-6 py-3 text-left font-medium">Type</th>
- <th class="px-6 py-3 text-left font-medium">Job</th>
- <th class="px-6 py-3 text-left font-medium">Date</th>
- <th class="px-6 py-3 text-left font-medium">Statut</th>
- <th class="px-6 py-3 text-right font-medium">Taille</th>
- <th class="px-6 py-3 text-right font-medium">Actions</th>
- </tr>
- </thead>
- <tbody class="divide-y divide-gray-100">
- {% for item in items %}
- <tr class="hover:bg-gray-50 archive-row"
- data-name="{{ item.name | lower }}"
- data-job="{{ item.job_name | lower }}"
- data-status="{{ item.last_status or '' }}"
- data-type="{{ item.type or '' }}">
- <td class="px-6 py-3 font-mono text-xs text-gray-700 max-w-xs truncate" title="{{ item.name }}">
- {{ item.name }}
- </td>
- <td class="px-6 py-3">
- {% if item.type %}
- <span class="bg-gray-100 text-gray-600 text-xs px-2 py-0.5 rounded font-mono">{{ item.type }}</span>
- {% else %}
- <span class="text-gray-300 text-xs">—</span>
- {% endif %}
- </td>
- <td class="px-6 py-3 text-xs text-gray-600">
- {% if item.job_id %}
- <a href="{{ url_for('jobs.job_history', job_id=item.job_id) }}"
- class="hover:text-blue-600 hover:underline">{{ item.job_name }}</a>
- {% else %}
- {{ item.job_name }}
- {% endif %}
- </td>
- <td class="px-6 py-3 text-xs text-gray-500 whitespace-nowrap">
- {% if item.run_at %}{{ item.run_at.strftime('%d/%m/%Y %H:%M') }}
- {% else %}<span class="text-gray-300">—</span>{% endif %}
- </td>
- <td class="px-6 py-3">
- {% if item.last_status == 'success' %}
- <span class="bg-green-100 text-green-700 text-xs font-medium px-2 py-0.5 rounded-full">✓ succès</span>
- {% elif item.last_status == 'error' %}
- <span class="bg-red-100 text-red-700 text-xs font-medium px-2 py-0.5 rounded-full">✗ erreur</span>
- {% elif item.last_status == 'running' %}
- <span class="bg-blue-100 text-blue-700 text-xs font-medium px-2 py-0.5 rounded-full animate-pulse">⟳ en cours</span>
- {% else %}
- <span class="text-gray-300 text-xs">—</span>
- {% endif %}
- </td>
- <td class="px-6 py-3 text-xs text-gray-500 text-right font-mono">{{ item.size_human }}</td>
- <td class="px-6 py-3 text-right">
- <div class="flex items-center justify-end gap-1.5">
- <a href="{{ url_for('jobs.archive_restore', archive_name=item.name) }}"
- class="btn-secondary btn-sm">↩ Restaurer</a>
- {% if instances %}
- <div class="relative group">
- <button type="button"
- class="btn-secondary btn-sm"
- onclick="togglePush(this)">↑ Pousser</button>
- <div class="push-menu hidden absolute right-0 top-full mt-1 z-20 bg-white border border-gray-200 rounded-lg shadow-lg min-w-40 py-1">
- {% for inst in instances %}
- <form method="post"
- action="{{ url_for('network.archive_push', archive_name=item.name, inst_id=inst.id) }}"
- onsubmit="return confirm('Pousser « {{ item.name }} » vers « {{ inst.name }} » ?')">
- <button type="submit"
- class="w-full text-left px-4 py-2 text-xs hover:bg-gray-50 text-gray-700">
- {{ inst.name }}
- </button>
- </form>
- {% endfor %}
- </div>
- </div>
- {% endif %}
- <a href="{{ url_for('jobs.archive_download', archive_name=item.name) }}"
- class="btn-ghost btn-icon-sm" title="Télécharger ({{ item.size_human }})"
- onclick="return confirm('Télécharger l\'archive « {{ item.name }} » ({{ item.size_human }}) ?')">↓</a>
- <form method="post"
- action="{{ url_for('jobs.archive_delete', archive_name=item.name) }}"
- onsubmit="return confirm('Supprimer définitivement l\'archive « {{ item.name }} » ?')">
- <button type="submit" class="btn-danger btn-icon-sm">✕</button>
- </form>
- </div>
- </td>
- </tr>
- {% endfor %}
- </tbody>
- </table>
- </div>
- <div id="empty-filter" class="hidden px-6 py-8 text-center text-gray-400 text-sm">
- Aucune archive ne correspond aux filtres.
- </div>
- </div>
- <script>
- function applyFilters() {
- const search = document.getElementById('filter-search').value.toLowerCase();
- const status = document.getElementById('filter-status').value;
- const type = document.getElementById('filter-type').value;
- let visible = 0;
- document.querySelectorAll('.archive-row').forEach(function(row) {
- const matchSearch = !search || row.dataset.name.includes(search) || row.dataset.job.includes(search);
- const matchStatus = !status || row.dataset.status === status;
- const matchType = !type || row.dataset.type === type;
- const show = matchSearch && matchStatus && matchType;
- row.classList.toggle('hidden', !show);
- if (show) visible++;
- });
- document.getElementById('empty-filter').classList.toggle('hidden', visible > 0);
- }
- function togglePush(btn) {
- const menu = btn.nextElementSibling;
- const allMenus = document.querySelectorAll('.push-menu');
- allMenus.forEach(function(m) { if (m !== menu) m.classList.add('hidden'); });
- menu.classList.toggle('hidden');
- }
- document.addEventListener('click', function(e) {
- if (!e.target.closest('.group')) {
- document.querySelectorAll('.push-menu').forEach(function(m) { m.classList.add('hidden'); });
- }
- });
- </script>
- {% endif %}
- {% endblock %}
|