archives.html 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182
  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. {% if item.app_id %}
  74. <br>
  75. {% if item.app_label and item.app_label != item.app_id %}
  76. <span class="text-xs text-gray-500">{{ item.app_label }}</span>
  77. {% else %}
  78. <span class="font-mono text-xs text-gray-400">{{ item.app_id }}</span>
  79. {% endif %}
  80. {% endif %}
  81. </td>
  82. <td class="px-6 py-3 text-xs text-gray-500 whitespace-nowrap">
  83. {% if item.run_at %}{{ item.run_at.strftime('%d/%m/%Y %H:%M') }}
  84. {% else %}<span class="text-gray-300">—</span>{% endif %}
  85. </td>
  86. <td class="px-6 py-3">
  87. {% if item.last_status == 'success' %}
  88. <span class="bg-green-100 text-green-700 text-xs font-medium px-2 py-0.5 rounded-full">✓ succès</span>
  89. {% elif item.last_status == 'error' %}
  90. <span class="bg-red-100 text-red-700 text-xs font-medium px-2 py-0.5 rounded-full">✗ erreur</span>
  91. {% elif item.last_status == 'running' %}
  92. <span class="bg-blue-100 text-blue-700 text-xs font-medium px-2 py-0.5 rounded-full animate-pulse">⟳ en cours</span>
  93. {% else %}
  94. <span class="text-gray-300 text-xs">—</span>
  95. {% endif %}
  96. </td>
  97. <td class="px-6 py-3 text-xs text-gray-500 text-right font-mono">{{ item.size_human }}</td>
  98. <td class="px-6 py-3 text-right">
  99. <div class="flex items-center justify-end gap-1.5">
  100. <a href="{{ url_for('jobs.archive_restore', archive_name=item.name) }}"
  101. class="btn-secondary btn-sm">↩ Restaurer</a>
  102. {% if instances %}
  103. <div class="relative group">
  104. <button type="button"
  105. class="btn-secondary btn-sm"
  106. onclick="togglePush(this)">↑ Pousser</button>
  107. <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">
  108. {% for inst in instances %}
  109. <form method="post"
  110. action="{{ url_for('network.archive_push', archive_name=item.name, inst_id=inst.id) }}"
  111. onsubmit="return confirm('Pousser « {{ item.name }} » vers « {{ inst.name }} » ?')">
  112. <button type="submit"
  113. class="w-full text-left px-4 py-2 text-xs hover:bg-gray-50 text-gray-700">
  114. {{ inst.name }}
  115. </button>
  116. </form>
  117. {% endfor %}
  118. </div>
  119. </div>
  120. {% endif %}
  121. <a href="{{ url_for('jobs.archive_download', archive_name=item.name) }}"
  122. class="btn-ghost btn-icon-sm" title="Télécharger ({{ item.size_human }})"
  123. onclick="return confirm('Télécharger l\'archive « {{ item.name }} » ({{ item.size_human }}) ?')">↓</a>
  124. <form method="post"
  125. action="{{ url_for('jobs.archive_delete', archive_name=item.name) }}"
  126. onsubmit="return confirm('Supprimer définitivement l\'archive « {{ item.name }} » ?')">
  127. <button type="submit" class="btn-danger btn-icon-sm">✕</button>
  128. </form>
  129. </div>
  130. </td>
  131. </tr>
  132. {% endfor %}
  133. </tbody>
  134. </table>
  135. </div>
  136. <div id="empty-filter" class="hidden px-6 py-8 text-center text-gray-400 text-sm">
  137. Aucune archive ne correspond aux filtres.
  138. </div>
  139. </div>
  140. <script>
  141. function applyFilters() {
  142. const search = document.getElementById('filter-search').value.toLowerCase();
  143. const status = document.getElementById('filter-status').value;
  144. const type = document.getElementById('filter-type').value;
  145. let visible = 0;
  146. document.querySelectorAll('.archive-row').forEach(function(row) {
  147. const matchSearch = !search || row.dataset.name.includes(search) || row.dataset.job.includes(search);
  148. const matchStatus = !status || row.dataset.status === status;
  149. const matchType = !type || row.dataset.type === type;
  150. const show = matchSearch && matchStatus && matchType;
  151. row.classList.toggle('hidden', !show);
  152. if (show) visible++;
  153. });
  154. document.getElementById('empty-filter').classList.toggle('hidden', visible > 0);
  155. }
  156. function togglePush(btn) {
  157. const menu = btn.nextElementSibling;
  158. const allMenus = document.querySelectorAll('.push-menu');
  159. allMenus.forEach(function(m) { if (m !== menu) m.classList.add('hidden'); });
  160. menu.classList.toggle('hidden');
  161. }
  162. document.addEventListener('click', function(e) {
  163. if (!e.target.closest('.group')) {
  164. document.querySelectorAll('.push-menu').forEach(function(m) { m.classList.add('hidden'); });
  165. }
  166. });
  167. </script>
  168. {% endif %}
  169. {% endblock %}