瀏覽代碼

feat: vue globale par job avec onglets (par job / par destination)

Ajoute un switch d'onglets sur la page vue globale :
- "Par job" (défaut) : chaque job affiche ses archives sur toutes les
  destinations avec un indicateur de réplication coloré (vert/amber/rouge)
- "Par destination" : vue existante inchangée
Le choix d'onglet est mémorisé dans localStorage.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Cedric Hansen 2 周之前
父節點
當前提交
59113eb00f
共有 2 個文件被更改,包括 266 次插入100 次删除
  1. 21 1
      sources/blueprints/overview.py
  2. 245 99
      sources/templates/archives_overview.html

+ 21 - 1
sources/blueprints/overview.py

@@ -178,7 +178,27 @@ def overview():
                 "error": str(exc), "groups": [], "orphaned": [],
             })
 
-    return render_template("archives_overview.html", locations=locations)
+    # --- Vue par job : pivot de locations → jobs ---
+    jobs_view = []
+    for job in jobs:
+        jlocs = []
+        for loc in locations:
+            group = next((g for g in loc.get("groups", []) if g["job"].id == job.id), None)
+            jlocs.append({
+                "type": loc["type"],
+                "label": loc["label"],
+                "dest_id": loc.get("dest_id"),
+                "instance_id": loc.get("instance_id"),
+                "error": loc["error"],
+                "archives": group["archives"] if group else [],
+            })
+        jobs_view.append({
+            "job": job,
+            "retention_label": _retention_label(job),
+            "locations": jlocs,
+        })
+
+    return render_template("archives_overview.html", locations=locations, jobs_view=jobs_view)
 
 
 @bp.route("/overview/delete", methods=["POST"])

+ 245 - 99
sources/templates/archives_overview.html

@@ -2,96 +2,105 @@
 {% block title %}Vue globale des archives{% endblock %}
 
 {% block content %}
-<div class="space-y-8">
+<div class="space-y-6">
 
+  <!-- En-tête -->
   <div class="flex items-center justify-between">
     <div>
       <h1 class="text-2xl font-bold text-gray-900">Vue globale des archives</h1>
       <p class="text-sm text-gray-500 mt-1">Toutes les destinations — local, SSH et instances fédérées</p>
     </div>
-    <a href="{{ url_for('overview.overview') }}" class="btn-ghost btn-sm">
-      ↻ Rafraîchir
-    </a>
+    <a href="{{ url_for('overview.overview') }}" class="btn-ghost btn-sm">↻ Rafraîchir</a>
   </div>
 
-  {% for loc in locations %}
-  <div class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
+  <!-- Onglets -->
+  <div class="flex gap-1 bg-gray-100 rounded-lg p-1 w-fit">
+    <button id="tab-jobs" onclick="switchTab('jobs')"
+            class="px-4 py-2 text-sm font-medium rounded-md transition">
+      Par job
+    </button>
+    <button id="tab-dests" onclick="switchTab('dests')"
+            class="px-4 py-2 text-sm font-medium rounded-md transition">
+      Par destination
+    </button>
+  </div>
 
-    {# En-tête de la section #}
-    <div class="px-6 py-4 bg-gray-50 border-b border-gray-200 flex items-center gap-3">
-      {% if loc.type == "local" %}
-        <span class="inline-flex items-center gap-1 bg-blue-100 text-blue-700 text-xs font-semibold px-2.5 py-1 rounded-full">
-          Local
-        </span>
-      {% elif loc.type == "ssh" %}
-        <span class="inline-flex items-center gap-1 bg-slate-100 text-slate-700 text-xs font-semibold px-2.5 py-1 rounded-full">
-          SSH
-        </span>
-      {% else %}
-        <span class="inline-flex items-center gap-1 bg-purple-100 text-purple-700 text-xs font-semibold px-2.5 py-1 rounded-full">
-          Instance
-        </span>
-      {% endif %}
-      <span class="font-semibold text-gray-800">{{ loc.label }}</span>
-      {% if loc.type == "instance" and loc.instance_url is defined %}
-        <span class="text-xs text-gray-400">{{ loc.instance_url }}</span>
-      {% endif %}
-    </div>
+  <!-- ================================================================
+       VUE PAR JOB (défaut)
+       ================================================================ -->
+  <div id="view-jobs" class="space-y-6">
 
-    {# Erreur de connexion #}
-    {% if loc.error %}
-    <div class="px-6 py-5 flex items-start gap-3 text-sm text-red-700 bg-red-50">
-      <span class="text-base shrink-0">⚠</span>
-      <span>{{ loc.error }}</span>
-    </div>
+    {% if not jobs_view %}
+    <div class="text-center text-sm text-gray-400 py-12">Aucun job configuré.</div>
+    {% endif %}
 
-    {% else %}
+    {% for item in jobs_view %}
+    <div class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
 
-      {# Groupes par job #}
-      {% set ns = namespace(has_content=false) %}
-      {% for group in loc.groups %}
-        {% if group.archives %}
-          {% set ns.has_content = true %}
-        {% endif %}
-      {% endfor %}
-      {% if loc.orphaned %}{% set ns.has_content = true %}{% endif %}
+      <!-- En-tête du job -->
+      <div class="px-6 py-4 bg-gray-50 border-b border-gray-200 flex items-center gap-3 flex-wrap">
+        <span class="font-semibold text-gray-800">{{ item.job.name }}</span>
+        <span class="text-xs bg-gray-100 text-gray-500 px-2 py-0.5 rounded-full">{{ item.job.type }}</span>
+        <span class="text-xs bg-green-50 text-green-700 border border-green-200 px-2 py-0.5 rounded-full">
+          rétention : {{ item.retention_label }}
+        </span>
 
-      {% if not ns.has_content %}
-      <div class="px-6 py-8 text-center text-sm text-gray-400">
-        Aucune archive sur cette destination.
+        <!-- Compteur de réplication -->
+        {% set ns = namespace(copies=0) %}
+        {% for loc in item.locations %}
+          {% if not loc.error and loc.archives %}{% set ns.copies = ns.copies + 1 %}{% endif %}
+        {% endfor %}
+        {% set total = item.locations | length %}
+        <span class="ml-auto text-xs font-semibold px-2.5 py-1 rounded-full
+          {% if ns.copies == total %}bg-green-100 text-green-700
+          {% elif ns.copies == 0 %}bg-red-100 text-red-600
+          {% else %}bg-amber-100 text-amber-700{% endif %}">
+          {{ ns.copies }}/{{ total }} destination{% if total > 1 %}s{% endif %}
+        </span>
       </div>
-      {% else %}
 
+      <!-- Destinations pour ce job -->
       <div class="divide-y divide-gray-100">
-
-        {% for group in loc.groups %}
-        {% if group.archives %}
+        {% for loc in item.locations %}
         <div class="px-6 py-4">
 
-          {# En-tête du groupe #}
-          <div class="flex items-center justify-between mb-3">
+          <!-- Ligne d'en-tête destination -->
+          <div class="flex items-center justify-between mb-3 flex-wrap gap-2">
             <div class="flex items-center gap-2">
-              <span class="font-medium text-gray-800">{{ group.job.name }}</span>
-              <span class="text-xs bg-gray-100 text-gray-500 px-2 py-0.5 rounded-full">
-                {{ group.job.type }}
-              </span>
-              <span class="text-xs bg-green-50 text-green-700 border border-green-200 px-2 py-0.5 rounded-full">
-                rétention : {{ group.retention_label }}
-              </span>
+              {% if loc.type == "local" %}
+                <span class="inline-flex items-center bg-blue-100 text-blue-700 text-xs font-semibold px-2.5 py-1 rounded-full">Local</span>
+              {% elif loc.type == "ssh" %}
+                <span class="inline-flex items-center bg-slate-100 text-slate-700 text-xs font-semibold px-2.5 py-1 rounded-full">SSH</span>
+              {% else %}
+                <span class="inline-flex items-center bg-purple-100 text-purple-700 text-xs font-semibold px-2.5 py-1 rounded-full">Instance</span>
+              {% endif %}
+              <span class="text-sm text-gray-700">{{ loc.label }}</span>
+              {% if not loc.error %}
+                <span class="text-xs px-2 py-0.5 rounded-full
+                  {% if loc.archives %}bg-green-50 text-green-700 border border-green-200
+                  {% else %}bg-gray-100 text-gray-400{% endif %}">
+                  {{ loc.archives | length }} archive{% if loc.archives | length != 1 %}s{% endif %}
+                </span>
+              {% endif %}
             </div>
+            {% if not loc.error %}
             <form method="post" action="{{ url_for('overview.overview_retention') }}"
-                  onsubmit="return confirm('Appliquer la rétention pour « {{ group.job.name }} » sur cette destination ?')">
-              <input type="hidden" name="job_id" value="{{ group.job.id }}">
+                  onsubmit="return confirm('Appliquer la rétention pour « {{ item.job.name }} » sur cette destination ?')">
+              <input type="hidden" name="job_id" value="{{ item.job.id }}">
               <input type="hidden" name="loc_type" value="{{ loc.type }}">
               {% if loc.type == "ssh" %}<input type="hidden" name="dest_id" value="{{ loc.dest_id }}">{% endif %}
               {% if loc.type == "instance" %}<input type="hidden" name="instance_id" value="{{ loc.instance_id }}">{% endif %}
-              <button type="submit" class="btn-secondary btn-sm">
-                ↺ Appliquer la rétention
-              </button>
+              <button type="submit" class="btn-secondary btn-sm">↺ Appliquer la rétention</button>
             </form>
+            {% endif %}
           </div>
 
-          {# Table des archives #}
+          <!-- Contenu : erreur / archives / vide -->
+          {% if loc.error %}
+          <div class="flex items-start gap-2 text-sm text-red-700 bg-red-50 px-4 py-3 rounded-lg">
+            <span class="shrink-0">⚠</span><span>{{ loc.error }}</span>
+          </div>
+          {% elif loc.archives %}
           <div class="rounded-lg border border-gray-100 overflow-hidden">
             <table class="w-full text-sm">
               <thead class="bg-gray-50 text-xs text-gray-500 uppercase tracking-wide">
@@ -103,7 +112,7 @@
                 </tr>
               </thead>
               <tbody class="divide-y divide-gray-50">
-                {% for arch in group.archives %}
+                {% for arch in loc.archives %}
                 <tr class="hover:bg-gray-50 transition">
                   <td class="px-4 py-2.5 font-mono text-xs text-gray-700">{{ arch.name }}</td>
                   <td class="px-4 py-2.5 text-right text-gray-500 tabular-nums">{{ arch.size_human }}</td>
@@ -123,52 +132,189 @@
               </tbody>
             </table>
           </div>
+          {% else %}
+          <div class="text-sm text-gray-400 italic">Aucune archive sur cette destination.</div>
+          {% endif %}
 
         </div>
+        {% endfor %}
+      </div>
+
+    </div>
+    {% endfor %}
+
+  </div><!-- fin #view-jobs -->
+
+
+  <!-- ================================================================
+       VUE PAR DESTINATION (existante)
+       ================================================================ -->
+  <div id="view-dests" class="hidden space-y-8">
+
+    {% for loc in locations %}
+    <div class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
+
+      <div class="px-6 py-4 bg-gray-50 border-b border-gray-200 flex items-center gap-3">
+        {% if loc.type == "local" %}
+          <span class="inline-flex items-center gap-1 bg-blue-100 text-blue-700 text-xs font-semibold px-2.5 py-1 rounded-full">Local</span>
+        {% elif loc.type == "ssh" %}
+          <span class="inline-flex items-center gap-1 bg-slate-100 text-slate-700 text-xs font-semibold px-2.5 py-1 rounded-full">SSH</span>
+        {% else %}
+          <span class="inline-flex items-center gap-1 bg-purple-100 text-purple-700 text-xs font-semibold px-2.5 py-1 rounded-full">Instance</span>
+        {% endif %}
+        <span class="font-semibold text-gray-800">{{ loc.label }}</span>
+        {% if loc.type == "instance" and loc.instance_url is defined %}
+          <span class="text-xs text-gray-400">{{ loc.instance_url }}</span>
         {% endif %}
+      </div>
+
+      {% if loc.error %}
+      <div class="px-6 py-5 flex items-start gap-3 text-sm text-red-700 bg-red-50">
+        <span class="text-base shrink-0">⚠</span>
+        <span>{{ loc.error }}</span>
+      </div>
+
+      {% else %}
+
+        {% set ns = namespace(has_content=false) %}
+        {% for group in loc.groups %}
+          {% if group.archives %}{% set ns.has_content = true %}{% endif %}
         {% endfor %}
+        {% if loc.orphaned %}{% set ns.has_content = true %}{% endif %}
 
-        {# Archives orphelines (ne correspondent à aucun job) #}
-        {% if loc.orphaned %}
-        <div class="px-6 py-4">
-          <div class="flex items-center gap-2 mb-3">
-            <span class="font-medium text-gray-500">Sans job associé</span>
-            <span class="text-xs bg-yellow-50 text-yellow-700 border border-yellow-200 px-2 py-0.5 rounded-full">
-              {{ loc.orphaned | length }} archive(s)
-            </span>
+        {% if not ns.has_content %}
+        <div class="px-6 py-8 text-center text-sm text-gray-400">
+          Aucune archive sur cette destination.
+        </div>
+        {% else %}
+
+        <div class="divide-y divide-gray-100">
+
+          {% for group in loc.groups %}
+          {% if group.archives %}
+          <div class="px-6 py-4">
+            <div class="flex items-center justify-between mb-3">
+              <div class="flex items-center gap-2">
+                <span class="font-medium text-gray-800">{{ group.job.name }}</span>
+                <span class="text-xs bg-gray-100 text-gray-500 px-2 py-0.5 rounded-full">{{ group.job.type }}</span>
+                <span class="text-xs bg-green-50 text-green-700 border border-green-200 px-2 py-0.5 rounded-full">
+                  rétention : {{ group.retention_label }}
+                </span>
+              </div>
+              <form method="post" action="{{ url_for('overview.overview_retention') }}"
+                    onsubmit="return confirm('Appliquer la rétention pour « {{ group.job.name }} » sur cette destination ?')">
+                <input type="hidden" name="job_id" value="{{ group.job.id }}">
+                <input type="hidden" name="loc_type" value="{{ loc.type }}">
+                {% if loc.type == "ssh" %}<input type="hidden" name="dest_id" value="{{ loc.dest_id }}">{% endif %}
+                {% if loc.type == "instance" %}<input type="hidden" name="instance_id" value="{{ loc.instance_id }}">{% endif %}
+                <button type="submit" class="btn-secondary btn-sm">↺ Appliquer la rétention</button>
+              </form>
+            </div>
+            <div class="rounded-lg border border-gray-100 overflow-hidden">
+              <table class="w-full text-sm">
+                <thead class="bg-gray-50 text-xs text-gray-500 uppercase tracking-wide">
+                  <tr>
+                    <th class="text-left px-4 py-2 font-medium">Archive</th>
+                    <th class="text-right px-4 py-2 font-medium">Taille</th>
+                    <th class="text-right px-4 py-2 font-medium">Date</th>
+                    <th class="px-4 py-2"></th>
+                  </tr>
+                </thead>
+                <tbody class="divide-y divide-gray-50">
+                  {% for arch in group.archives %}
+                  <tr class="hover:bg-gray-50 transition">
+                    <td class="px-4 py-2.5 font-mono text-xs text-gray-700">{{ arch.name }}</td>
+                    <td class="px-4 py-2.5 text-right text-gray-500 tabular-nums">{{ arch.size_human }}</td>
+                    <td class="px-4 py-2.5 text-right text-gray-500 tabular-nums">{{ arch.date_str }}</td>
+                    <td class="px-4 py-2.5 text-right">
+                      <form method="post" action="{{ url_for('overview.overview_delete') }}"
+                            onsubmit="return confirm('Supprimer « {{ arch.name }} » ?')">
+                        <input type="hidden" name="name" value="{{ arch.name }}">
+                        <input type="hidden" name="loc_type" value="{{ loc.type }}">
+                        {% if loc.type == "ssh" %}<input type="hidden" name="dest_id" value="{{ loc.dest_id }}">{% endif %}
+                        {% if loc.type == "instance" %}<input type="hidden" name="instance_id" value="{{ loc.instance_id }}">{% endif %}
+                        <button type="submit" class="btn-danger btn-icon-sm">✕</button>
+                      </form>
+                    </td>
+                  </tr>
+                  {% endfor %}
+                </tbody>
+              </table>
+            </div>
           </div>
-          <div class="rounded-lg border border-gray-100 overflow-hidden">
-            <table class="w-full text-sm">
-              <tbody class="divide-y divide-gray-50">
-                {% for arch in loc.orphaned %}
-                <tr class="hover:bg-gray-50 transition">
-                  <td class="px-4 py-2.5 font-mono text-xs text-gray-700">{{ arch.name }}</td>
-                  <td class="px-4 py-2.5 text-right text-gray-500 tabular-nums">{{ arch.size_human }}</td>
-                  <td class="px-4 py-2.5 text-right text-gray-500 tabular-nums">{{ arch.date_str }}</td>
-                  <td class="px-4 py-2.5 text-right">
-                    <form method="post" action="{{ url_for('overview.overview_delete') }}"
-                          onsubmit="return confirm('Supprimer « {{ arch.name }} » ?')">
-                      <input type="hidden" name="name" value="{{ arch.name }}">
-                      <input type="hidden" name="loc_type" value="{{ loc.type }}">
-                      {% if loc.type == "ssh" %}<input type="hidden" name="dest_id" value="{{ loc.dest_id }}">{% endif %}
-                      {% if loc.type == "instance" %}<input type="hidden" name="instance_id" value="{{ loc.instance_id }}">{% endif %}
-                      <button type="submit" class="btn-danger btn-icon-sm">✕</button>
-                    </form>
-                  </td>
-                </tr>
-                {% endfor %}
-              </tbody>
-            </table>
+          {% endif %}
+          {% endfor %}
+
+          {% if loc.orphaned %}
+          <div class="px-6 py-4">
+            <div class="flex items-center gap-2 mb-3">
+              <span class="font-medium text-gray-500">Sans job associé</span>
+              <span class="text-xs bg-yellow-50 text-yellow-700 border border-yellow-200 px-2 py-0.5 rounded-full">
+                {{ loc.orphaned | length }} archive(s)
+              </span>
+            </div>
+            <div class="rounded-lg border border-gray-100 overflow-hidden">
+              <table class="w-full text-sm">
+                <tbody class="divide-y divide-gray-50">
+                  {% for arch in loc.orphaned %}
+                  <tr class="hover:bg-gray-50 transition">
+                    <td class="px-4 py-2.5 font-mono text-xs text-gray-700">{{ arch.name }}</td>
+                    <td class="px-4 py-2.5 text-right text-gray-500 tabular-nums">{{ arch.size_human }}</td>
+                    <td class="px-4 py-2.5 text-right text-gray-500 tabular-nums">{{ arch.date_str }}</td>
+                    <td class="px-4 py-2.5 text-right">
+                      <form method="post" action="{{ url_for('overview.overview_delete') }}"
+                            onsubmit="return confirm('Supprimer « {{ arch.name }} » ?')">
+                        <input type="hidden" name="name" value="{{ arch.name }}">
+                        <input type="hidden" name="loc_type" value="{{ loc.type }}">
+                        {% if loc.type == "ssh" %}<input type="hidden" name="dest_id" value="{{ loc.dest_id }}">{% endif %}
+                        {% if loc.type == "instance" %}<input type="hidden" name="instance_id" value="{{ loc.instance_id }}">{% endif %}
+                        <button type="submit" class="btn-danger btn-icon-sm">✕</button>
+                      </form>
+                    </td>
+                  </tr>
+                  {% endfor %}
+                </tbody>
+              </table>
+            </div>
           </div>
+          {% endif %}
+
         </div>
         {% endif %}
-
-      </div>
       {% endif %}
-    {% endif %}
 
-  </div>
-  {% endfor %}
+    </div>
+    {% endfor %}
+
+  </div><!-- fin #view-dests -->
 
 </div>
 {% endblock %}
+
+{% block scripts %}
+<script>
+function switchTab(name) {
+  const isJobs = name === 'jobs';
+  document.getElementById('view-jobs').classList.toggle('hidden', !isJobs);
+  document.getElementById('view-dests').classList.toggle('hidden', isJobs);
+
+  ['jobs', 'dests'].forEach(function(t) {
+    var btn = document.getElementById('tab-' + t);
+    var active = t === name;
+    btn.classList.toggle('bg-white', active);
+    btn.classList.toggle('text-gray-900', active);
+    btn.classList.toggle('shadow-sm', active);
+    btn.classList.toggle('font-semibold', active);
+    btn.classList.toggle('text-gray-500', !active);
+    btn.classList.toggle('hover:text-gray-700', !active);
+  });
+
+  try { localStorage.setItem('overview-tab', name); } catch(e) {}
+}
+
+// Restaurer l'onglet préféré (ou 'jobs' par défaut)
+var savedTab = 'jobs';
+try { savedTab = localStorage.getItem('overview-tab') || 'jobs'; } catch(e) {}
+switchTab(savedTab);
+</script>
+{% endblock %}