archives.html 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174
  1. {% extends "base.html" %}
  2. {% block title %}Archives{% endblock %}
  3. {% block content %}
  4. <div class="flex items-center justify-between mb-6">
  5. <h1 class="text-xl font-bold text-gray-900">Archives</h1>
  6. <span class="text-sm text-gray-400">{{ items | length }} archive{{ 's' if items | length != 1 }}</span>
  7. </div>
  8. {% if not items %}
  9. <div class="bg-white rounded-xl border border-gray-200 px-6 py-12 text-center text-gray-400">
  10. <p class="text-lg">Aucune archive sur ce serveur.</p>
  11. <p class="text-sm mt-2">Les archives apparaissent ici après l'exécution d'un job de sauvegarde.</p>
  12. </div>
  13. {% else %}
  14. {# ── Filtres ─────────────────────────────────────────────────────── #}
  15. <div class="bg-white rounded-xl border border-gray-200 p-4 mb-4 flex flex-wrap gap-3 items-center">
  16. <input id="filter-search" type="text" placeholder="Filtrer par nom ou job…"
  17. oninput="applyFilters()"
  18. 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">
  19. <select id="filter-status" onchange="applyFilters()"
  20. class="border border-gray-200 rounded px-3 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-blue-400">
  21. <option value="">Tous les statuts</option>
  22. <option value="success">✓ succès</option>
  23. <option value="error">✗ erreur</option>
  24. <option value="running">⟳ en cours</option>
  25. <option value="">— sans run</option>
  26. </select>
  27. <select id="filter-type" onchange="applyFilters()"
  28. class="border border-gray-200 rounded px-3 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-blue-400">
  29. <option value="">Tous les types</option>
  30. {% for t in items | map(attribute='type') | unique | sort %}
  31. {% if t %}<option value="{{ t }}">{{ t }}</option>{% endif %}
  32. {% endfor %}
  33. </select>
  34. </div>
  35. <div class="bg-white rounded-xl border border-gray-200 shadow-sm overflow-hidden">
  36. <div class="overflow-x-auto">
  37. <table class="w-full text-sm" id="archives-table">
  38. <thead>
  39. <tr class="text-xs text-gray-500 uppercase tracking-wide bg-gray-50">
  40. <th class="px-6 py-3 text-left font-medium">Archive</th>
  41. <th class="px-6 py-3 text-left font-medium">Type</th>
  42. <th class="px-6 py-3 text-left font-medium">Job</th>
  43. <th class="px-6 py-3 text-left font-medium">Date</th>
  44. <th class="px-6 py-3 text-left font-medium">Statut</th>
  45. <th class="px-6 py-3 text-right font-medium">Taille</th>
  46. <th class="px-6 py-3 text-right font-medium">Actions</th>
  47. </tr>
  48. </thead>
  49. <tbody class="divide-y divide-gray-100">
  50. {% for item in items %}
  51. <tr class="hover:bg-gray-50 archive-row"
  52. data-name="{{ item.name | lower }}"
  53. data-job="{{ item.job_name | lower }}"
  54. data-status="{{ item.last_status or '' }}"
  55. data-type="{{ item.type or '' }}">
  56. <td class="px-6 py-3 font-mono text-xs text-gray-700 max-w-xs truncate" title="{{ item.name }}">
  57. {{ item.name }}
  58. </td>
  59. <td class="px-6 py-3">
  60. {% if item.type %}
  61. <span class="bg-gray-100 text-gray-600 text-xs px-2 py-0.5 rounded font-mono">{{ item.type }}</span>
  62. {% else %}
  63. <span class="text-gray-300 text-xs">—</span>
  64. {% endif %}
  65. </td>
  66. <td class="px-6 py-3 text-xs text-gray-600">
  67. {% if item.job_id %}
  68. <a href="{{ url_for('jobs.job_history', job_id=item.job_id) }}"
  69. class="hover:text-blue-600 hover:underline">{{ item.job_name }}</a>
  70. {% else %}
  71. {{ item.job_name }}
  72. {% endif %}
  73. </td>
  74. <td class="px-6 py-3 text-xs text-gray-500 whitespace-nowrap">
  75. {% if item.run_at %}{{ item.run_at.strftime('%d/%m/%Y %H:%M') }}
  76. {% else %}<span class="text-gray-300">—</span>{% endif %}
  77. </td>
  78. <td class="px-6 py-3">
  79. {% if item.last_status == 'success' %}
  80. <span class="bg-green-100 text-green-700 text-xs font-medium px-2 py-0.5 rounded-full">✓ succès</span>
  81. {% elif item.last_status == 'error' %}
  82. <span class="bg-red-100 text-red-700 text-xs font-medium px-2 py-0.5 rounded-full">✗ erreur</span>
  83. {% elif item.last_status == 'running' %}
  84. <span class="bg-blue-100 text-blue-700 text-xs font-medium px-2 py-0.5 rounded-full animate-pulse">⟳ en cours</span>
  85. {% else %}
  86. <span class="text-gray-300 text-xs">—</span>
  87. {% endif %}
  88. </td>
  89. <td class="px-6 py-3 text-xs text-gray-500 text-right font-mono">{{ item.size_human }}</td>
  90. <td class="px-6 py-3 text-right">
  91. <div class="flex items-center justify-end gap-1.5">
  92. <a href="{{ url_for('jobs.archive_restore', archive_name=item.name) }}"
  93. class="btn-secondary btn-sm">↩ Restaurer</a>
  94. {% if instances %}
  95. <div class="relative group">
  96. <button type="button"
  97. class="btn-secondary btn-sm"
  98. onclick="togglePush(this)">↑ Pousser</button>
  99. <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">
  100. {% for inst in instances %}
  101. <form method="post"
  102. action="{{ url_for('network.archive_push', archive_name=item.name, inst_id=inst.id) }}"
  103. onsubmit="return confirm('Pousser « {{ item.name }} » vers « {{ inst.name }} » ?')">
  104. <button type="submit"
  105. class="w-full text-left px-4 py-2 text-xs hover:bg-gray-50 text-gray-700">
  106. {{ inst.name }}
  107. </button>
  108. </form>
  109. {% endfor %}
  110. </div>
  111. </div>
  112. {% endif %}
  113. <a href="{{ url_for('jobs.archive_download', archive_name=item.name) }}"
  114. class="btn-ghost btn-icon-sm" title="Télécharger ({{ item.size_human }})"
  115. onclick="return confirm('Télécharger l\'archive « {{ item.name }} » ({{ item.size_human }}) ?')">↓</a>
  116. <form method="post"
  117. action="{{ url_for('jobs.archive_delete', archive_name=item.name) }}"
  118. onsubmit="return confirm('Supprimer définitivement l\'archive « {{ item.name }} » ?')">
  119. <button type="submit" class="btn-danger btn-icon-sm">✕</button>
  120. </form>
  121. </div>
  122. </td>
  123. </tr>
  124. {% endfor %}
  125. </tbody>
  126. </table>
  127. </div>
  128. <div id="empty-filter" class="hidden px-6 py-8 text-center text-gray-400 text-sm">
  129. Aucune archive ne correspond aux filtres.
  130. </div>
  131. </div>
  132. <script>
  133. function applyFilters() {
  134. const search = document.getElementById('filter-search').value.toLowerCase();
  135. const status = document.getElementById('filter-status').value;
  136. const type = document.getElementById('filter-type').value;
  137. let visible = 0;
  138. document.querySelectorAll('.archive-row').forEach(function(row) {
  139. const matchSearch = !search || row.dataset.name.includes(search) || row.dataset.job.includes(search);
  140. const matchStatus = !status || row.dataset.status === status;
  141. const matchType = !type || row.dataset.type === type;
  142. const show = matchSearch && matchStatus && matchType;
  143. row.classList.toggle('hidden', !show);
  144. if (show) visible++;
  145. });
  146. document.getElementById('empty-filter').classList.toggle('hidden', visible > 0);
  147. }
  148. function togglePush(btn) {
  149. const menu = btn.nextElementSibling;
  150. const allMenus = document.querySelectorAll('.push-menu');
  151. allMenus.forEach(function(m) { if (m !== menu) m.classList.add('hidden'); });
  152. menu.classList.toggle('hidden');
  153. }
  154. document.addEventListener('click', function(e) {
  155. if (!e.target.closest('.group')) {
  156. document.querySelectorAll('.push-menu').forEach(function(m) { m.classList.add('hidden'); });
  157. }
  158. });
  159. </script>
  160. {% endif %}
  161. {% endblock %}