base.html 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
  1. <!DOCTYPE html>
  2. <html lang="fr" class="h-full bg-gray-50">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <title>{% block title %}Backup Manager{% endblock %} — {{ instance_name }}</title>
  7. <link rel="icon" type="image/png" href="{{ url_for('static', filename='icon.png') }}">
  8. <link rel="apple-touch-icon" href="{{ url_for('static', filename='icon.png') }}">
  9. <script src="https://cdn.tailwindcss.com"></script>
  10. <style type="text/tailwindcss">
  11. @layer components {
  12. /* Bouton principal (créer, lancer, enregistrer) — bleu */
  13. .btn-primary { @apply inline-flex items-center gap-1.5 bg-blue-600 hover:bg-blue-700 text-white font-semibold rounded-full shadow-sm transition; }
  14. /* Bouton secondaire (tester, éditer, sync, historique) — gris ardoise */
  15. .btn-secondary { @apply inline-flex items-center gap-1.5 bg-slate-100 hover:bg-slate-200 text-slate-700 font-medium rounded-full transition; }
  16. /* Bouton action légère (rapatrier, toggle, télécharger) — gris neutre */
  17. .btn-ghost { @apply inline-flex items-center gap-1 bg-gray-100 hover:bg-gray-200 text-gray-600 font-medium rounded-full transition; }
  18. /* Bouton destructeur — rouge */
  19. .btn-danger { @apply inline-flex items-center gap-1 bg-red-100 hover:bg-red-200 text-red-600 font-medium rounded-full transition; }
  20. /* Tailles */
  21. .btn-sm { @apply text-xs px-3 py-1.5; }
  22. .btn-md { @apply text-sm px-5 py-2; }
  23. .btn-icon-sm { @apply text-xs px-2 py-1; }
  24. }
  25. </style>
  26. </head>
  27. <body class="h-full flex flex-col">
  28. <div class="sticky top-0 z-50">
  29. <nav class="bg-gray-900 text-white shadow-lg">
  30. <div class="max-w-7xl mx-auto px-6 py-3 flex items-center justify-between">
  31. <div class="flex items-center gap-3">
  32. <img src="{{ url_for('static', filename='icon.png') }}" alt="logo" class="w-7 h-7 rounded">
  33. <a href="{{ url_for('jobs.index') }}" class="text-lg font-bold tracking-tight">Backup Manager</a>
  34. <span class="bg-blue-600 text-xs font-medium px-2 py-0.5 rounded">{{ instance_name }}</span>
  35. </div>
  36. <div class="flex items-center gap-4 text-sm">
  37. <a href="{{ url_for('jobs.index') }}"
  38. class="text-gray-300 hover:text-white transition {% if request.endpoint and request.endpoint.startswith('jobs.') and request.endpoint != 'jobs.job_new' %}text-white font-semibold{% endif %}">
  39. Dashboard
  40. </a>
  41. <a href="{{ url_for('jobs.archives') }}"
  42. class="text-gray-300 hover:text-white transition {% if request.endpoint == 'jobs.archives' %}text-white font-semibold{% endif %}">
  43. Archives
  44. </a>
  45. <a href="{{ url_for('cfg.settings') }}"
  46. class="text-gray-300 hover:text-white transition {% if request.endpoint and request.endpoint.startswith('cfg.') %}text-white font-semibold{% endif %}">
  47. Paramètres
  48. </a>
  49. <a href="{{ url_for('jobs.job_new') }}"
  50. class="bg-blue-600 hover:bg-blue-700 text-white px-3 py-1.5 rounded font-medium transition">
  51. + Nouveau job
  52. </a>
  53. </div>
  54. </div>
  55. </nav>
  56. <!-- Barre d'activité (dans le bloc sticky avec la navbar) -->
  57. <div id="activity-bar"
  58. class="hidden bg-gray-900 border-b border-blue-700 text-white text-xs px-6 py-2 flex items-center justify-center gap-3 shadow">
  59. <span class="inline-block w-2 h-2 rounded-full bg-blue-400 animate-pulse shrink-0"></span>
  60. <span class="text-blue-300 font-medium shrink-0">En cours</span>
  61. <div id="activity-list" class="flex gap-4 flex-wrap text-gray-200"></div>
  62. </div><!-- fin sticky -->
  63. <main class="flex-1 max-w-7xl mx-auto w-full px-6 py-8">
  64. {% with messages = get_flashed_messages(with_categories=true) %}
  65. {% if messages %}
  66. <div class="mb-6 space-y-2">
  67. {% for category, message in messages %}
  68. <div class="px-4 py-3 rounded-lg text-sm font-medium
  69. {% if category == 'error' %}bg-red-50 text-red-800 border border-red-200
  70. {% elif category == 'info' %}bg-blue-50 text-blue-800 border border-blue-200
  71. {% else %}bg-green-50 text-green-800 border border-green-200{% endif %}">
  72. {{ message }}
  73. </div>
  74. {% endfor %}
  75. </div>
  76. {% endif %}
  77. {% endwith %}
  78. {% block content %}{% endblock %}
  79. </main>
  80. <footer class="text-center text-xs text-gray-400 py-4">
  81. Backup Manager — instance <strong>{{ instance_name }}</strong>
  82. </footer>
  83. {% block scripts %}{% endblock %}
  84. <script>
  85. (function() {
  86. const bar = document.getElementById('activity-bar');
  87. const list = document.getElementById('activity-list');
  88. const LABELS = { backup: 'Sauvegarde', restore: 'Restauration', push: 'Push', pull: 'Pull' };
  89. function elapsed(isoStr) {
  90. const sec = Math.round((Date.now() - new Date(isoStr).getTime()) / 1000);
  91. if (sec < 60) return sec + 's';
  92. return Math.floor(sec / 60) + 'min' + (sec % 60 ? (sec % 60) + 's' : '');
  93. }
  94. let prevCount = 0;
  95. function render(items) {
  96. const count = items ? items.length : 0;
  97. // Un job vient de se terminer → recharger la page pour mettre à jour les statuts
  98. if (prevCount > 0 && count < prevCount) {
  99. setTimeout(() => location.reload(), 800);
  100. }
  101. prevCount = count;
  102. if (count === 0) {
  103. bar.classList.add('hidden');
  104. return;
  105. }
  106. bar.classList.remove('hidden');
  107. list.innerHTML = items.map(it => {
  108. const label = LABELS[it.kind] || it.kind;
  109. const since = it.started_at ? ' · ' + elapsed(it.started_at) : '';
  110. const size = it.size_human ? ' · ' + it.size_human : '';
  111. return `<span class="bg-gray-800 rounded px-2 py-0.5">${label} <strong>${it.name}</strong>${since}${size}</span>`;
  112. }).join('');
  113. }
  114. function poll() {
  115. fetch('{{ url_for("api.api_running") }}', {
  116. headers: { 'X-BackupManager-Key': '{{ api_token }}' }
  117. })
  118. .then(r => r.ok ? r.json() : null)
  119. .then(data => render(data && data.running ? data.running : []))
  120. .catch(() => {});
  121. }
  122. poll();
  123. setInterval(poll, 5000);
  124. })();
  125. </script>
  126. </body>
  127. </html>