archives.html 8.3 KB

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