From f29de3616cf76af0dd8756a83335559e3e59272b Mon Sep 17 00:00:00 2001
From: Matthias Nott <mnott@mnsoft.org>
Date: Sun, 22 Feb 2026 00:30:09 +0100
Subject: [PATCH] feat: add project/env filters to backups page

---
 static/js/app.js |   78 ++++++++++++++++++++++++++++++++++-----
 1 files changed, 68 insertions(+), 10 deletions(-)

diff --git a/static/js/app.js b/static/js/app.js
index 0415eb7..69566b8 100644
--- a/static/js/app.js
+++ b/static/js/app.js
@@ -18,6 +18,10 @@
 let refreshTimer = null;
 const REFRESH_INTERVAL = 30000;
 
+// Backup filter state
+let backupFilterProject = null;  // null = all
+let backupFilterEnv = null;      // null = all
+
 // Log modal state
 let logCtx = { project: null, env: null, service: null };
 
@@ -459,6 +463,15 @@
 // ---------------------------------------------------------------------------
 // Backups
 // ---------------------------------------------------------------------------
+function fmtBackupDate(raw) {
+  if (!raw) return '\u2014';
+  // YYYYMMDD_HHMMSS -> YYYY-MM-DD HH:MM
+  const m = String(raw).match(/^(\d{4})(\d{2})(\d{2})[_T](\d{2})(\d{2})/);
+  if (m) return `${m[1]}-${m[2]}-${m[3]} ${m[4]}:${m[5]}`;
+  // YYYY-MM-DD passthrough
+  return raw;
+}
+
 async function renderBackups() {
   updateBreadcrumbs();
   const c = document.getElementById('page-content');
@@ -467,6 +480,18 @@
       api('/api/backups/'),
       api('/api/backups/offsite').catch(() => []),
     ]);
+
+    // Apply filters
+    const filteredLocal = local.filter(b => {
+      if (backupFilterProject && b.project !== backupFilterProject) return false;
+      if (backupFilterEnv && (b.env || b.environment || '') !== backupFilterEnv) return false;
+      return true;
+    });
+    const filteredOffsite = offsite.filter(b => {
+      if (backupFilterProject && b.project !== backupFilterProject) return false;
+      if (backupFilterEnv && (b.env || b.environment || '') !== backupFilterEnv) return false;
+      return true;
+    });
 
     let h = '<div class="page-enter">';
 
@@ -481,26 +506,53 @@
     }
     h += '</div></div>';
 
+    // Filter bar
+    const activeStyle = 'background:rgba(59,130,246,0.2);color:#60a5fa;';
+    h += '<div style="display:flex;flex-wrap:wrap;gap:0.5rem;align-items:center;margin-bottom:1.5rem;padding:0.75rem 1rem;background:#1f2937;border-radius:0.5rem;">';
+    h += '<span style="color:#9ca3af;font-size:0.875rem;margin-right:0.25rem;">Project:</span>';
+    h += `<button class="btn btn-ghost btn-xs" style="${backupFilterProject === null ? activeStyle : ''}" onclick="setBackupFilter('project',null)">All</button>`;
+    h += `<button class="btn btn-ghost btn-xs" style="${backupFilterProject === 'mdf' ? activeStyle : ''}" onclick="setBackupFilter('project','mdf')">mdf</button>`;
+    h += `<button class="btn btn-ghost btn-xs" style="${backupFilterProject === 'seriousletter' ? activeStyle : ''}" onclick="setBackupFilter('project','seriousletter')">seriousletter</button>`;
+    h += '<span style="color:#374151;margin:0 0.25rem;">|</span>';
+    h += '<span style="color:#9ca3af;font-size:0.875rem;margin-right:0.25rem;">Env:</span>';
+    h += `<button class="btn btn-ghost btn-xs" style="${backupFilterEnv === null ? activeStyle : ''}" onclick="setBackupFilter('env',null)">All</button>`;
+    h += `<button class="btn btn-ghost btn-xs" style="${backupFilterEnv === 'dev' ? activeStyle : ''}" onclick="setBackupFilter('env','dev')">dev</button>`;
+    h += `<button class="btn btn-ghost btn-xs" style="${backupFilterEnv === 'int' ? activeStyle : ''}" onclick="setBackupFilter('env','int')">int</button>`;
+    h += `<button class="btn btn-ghost btn-xs" style="${backupFilterEnv === 'prod' ? activeStyle : ''}" onclick="setBackupFilter('env','prod')">prod</button>`;
+    h += '</div>';
+
     // Local
     h += '<h2 style="font-size:1.125rem;font-weight:600;color:#f3f4f6;margin-bottom:0.75rem;">Local Backups</h2>';
-    if (local.length === 0) {
-      h += '<div class="card" style="color:#6b7280;">No local backups found.</div>';
+    if (filteredLocal.length === 0) {
+      h += '<div class="card" style="color:#6b7280;">No local backups match the current filter.</div>';
     } else {
-      h += '<div class="table-wrapper"><table class="ops-table"><thead><tr><th>Project</th><th>Env</th><th>Date</th><th>Size</th><th>Files</th></tr></thead><tbody>';
-      for (const b of local) {
-        h += `<tr><td>${esc(b.project||'')}</td><td><span class="badge badge-blue">${esc(b.env||b.environment||'')}</span></td><td>${esc(b.date||b.timestamp||'')}</td><td>${esc(b.size||'')}</td><td class="mono" style="font-size:0.75rem;">${esc(b.file||b.files||'')}</td></tr>`;
+      h += '<div class="table-wrapper"><table class="ops-table"><thead><tr><th>Project</th><th>Env</th><th>File</th><th>Date</th><th>Size</th></tr></thead><tbody>';
+      for (const b of filteredLocal) {
+        h += `<tr>
+          <td>${esc(b.project||'')}</td>
+          <td><span class="badge badge-blue">${esc(b.env||b.environment||'')}</span></td>
+          <td class="mono" style="font-size:0.8125rem;">${esc(b.name||b.file||'')}</td>
+          <td>${esc(fmtBackupDate(b.date||b.timestamp||''))}</td>
+          <td>${esc(b.size_human||b.size||'')}</td>
+        </tr>`;
       }
       h += '</tbody></table></div>';
     }
 
     // Offsite
     h += '<h2 style="font-size:1.125rem;font-weight:600;color:#f3f4f6;margin:1.5rem 0 0.75rem;">Offsite Backups</h2>';
-    if (offsite.length === 0) {
-      h += '<div class="card" style="color:#6b7280;">No offsite backups found.</div>';
+    if (filteredOffsite.length === 0) {
+      h += '<div class="card" style="color:#6b7280;">No offsite backups match the current filter.</div>';
     } else {
-      h += '<div class="table-wrapper"><table class="ops-table"><thead><tr><th>Project</th><th>Env</th><th>Date</th><th>Size</th></tr></thead><tbody>';
-      for (const b of offsite) {
-        h += `<tr><td>${esc(b.project||'')}</td><td><span class="badge badge-blue">${esc(b.env||b.environment||'')}</span></td><td>${esc(b.date||b.timestamp||'')}</td><td>${esc(b.size||'')}</td></tr>`;
+      h += '<div class="table-wrapper"><table class="ops-table"><thead><tr><th>Project</th><th>Env</th><th>File</th><th>Date</th><th>Size</th></tr></thead><tbody>';
+      for (const b of filteredOffsite) {
+        h += `<tr>
+          <td>${esc(b.project||'')}</td>
+          <td><span class="badge badge-blue">${esc(b.env||b.environment||'')}</span></td>
+          <td class="mono" style="font-size:0.8125rem;">${esc(b.name||'')}</td>
+          <td>${esc(fmtBackupDate(b.date||''))}</td>
+          <td>${esc(b.size||'')}</td>
+        </tr>`;
       }
       h += '</tbody></table></div>';
     }
@@ -704,6 +756,12 @@
   logCtx = { project: null, env: null, service: null };
 }
 
+function setBackupFilter(type, value) {
+  if (type === 'project') backupFilterProject = value;
+  if (type === 'env') backupFilterEnv = value;
+  renderBackups();
+}
+
 async function createBackup(project, env) {
   if (!confirm(`Create backup for ${project}/${env}?`)) return;
   toast('Creating backup...', 'info');

--
Gitblit v1.3.1