Matthias Nott
2026-02-22 f29de3616cf76af0dd8756a83335559e3e59272b
feat: add project/env filters to backups page
1 files modified
changed files
static/js/app.js patch | view | blame | history
static/js/app.js
....@@ -18,6 +18,10 @@
1818 let refreshTimer = null;
1919 const REFRESH_INTERVAL = 30000;
2020
21
+// Backup filter state
22
+let backupFilterProject = null; // null = all
23
+let backupFilterEnv = null; // null = all
24
+
2125 // Log modal state
2226 let logCtx = { project: null, env: null, service: null };
2327
....@@ -459,6 +463,15 @@
459463 // ---------------------------------------------------------------------------
460464 // Backups
461465 // ---------------------------------------------------------------------------
466
+function fmtBackupDate(raw) {
467
+ if (!raw) return '\u2014';
468
+ // YYYYMMDD_HHMMSS -> YYYY-MM-DD HH:MM
469
+ const m = String(raw).match(/^(\d{4})(\d{2})(\d{2})[_T](\d{2})(\d{2})/);
470
+ if (m) return `${m[1]}-${m[2]}-${m[3]} ${m[4]}:${m[5]}`;
471
+ // YYYY-MM-DD passthrough
472
+ return raw;
473
+}
474
+
462475 async function renderBackups() {
463476 updateBreadcrumbs();
464477 const c = document.getElementById('page-content');
....@@ -467,6 +480,18 @@
467480 api('/api/backups/'),
468481 api('/api/backups/offsite').catch(() => []),
469482 ]);
483
+
484
+ // Apply filters
485
+ const filteredLocal = local.filter(b => {
486
+ if (backupFilterProject && b.project !== backupFilterProject) return false;
487
+ if (backupFilterEnv && (b.env || b.environment || '') !== backupFilterEnv) return false;
488
+ return true;
489
+ });
490
+ const filteredOffsite = offsite.filter(b => {
491
+ if (backupFilterProject && b.project !== backupFilterProject) return false;
492
+ if (backupFilterEnv && (b.env || b.environment || '') !== backupFilterEnv) return false;
493
+ return true;
494
+ });
470495
471496 let h = '<div class="page-enter">';
472497
....@@ -481,26 +506,53 @@
481506 }
482507 h += '</div></div>';
483508
509
+ // Filter bar
510
+ const activeStyle = 'background:rgba(59,130,246,0.2);color:#60a5fa;';
511
+ 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;">';
512
+ h += '<span style="color:#9ca3af;font-size:0.875rem;margin-right:0.25rem;">Project:</span>';
513
+ h += `<button class="btn btn-ghost btn-xs" style="${backupFilterProject === null ? activeStyle : ''}" onclick="setBackupFilter('project',null)">All</button>`;
514
+ h += `<button class="btn btn-ghost btn-xs" style="${backupFilterProject === 'mdf' ? activeStyle : ''}" onclick="setBackupFilter('project','mdf')">mdf</button>`;
515
+ h += `<button class="btn btn-ghost btn-xs" style="${backupFilterProject === 'seriousletter' ? activeStyle : ''}" onclick="setBackupFilter('project','seriousletter')">seriousletter</button>`;
516
+ h += '<span style="color:#374151;margin:0 0.25rem;">|</span>';
517
+ h += '<span style="color:#9ca3af;font-size:0.875rem;margin-right:0.25rem;">Env:</span>';
518
+ h += `<button class="btn btn-ghost btn-xs" style="${backupFilterEnv === null ? activeStyle : ''}" onclick="setBackupFilter('env',null)">All</button>`;
519
+ h += `<button class="btn btn-ghost btn-xs" style="${backupFilterEnv === 'dev' ? activeStyle : ''}" onclick="setBackupFilter('env','dev')">dev</button>`;
520
+ h += `<button class="btn btn-ghost btn-xs" style="${backupFilterEnv === 'int' ? activeStyle : ''}" onclick="setBackupFilter('env','int')">int</button>`;
521
+ h += `<button class="btn btn-ghost btn-xs" style="${backupFilterEnv === 'prod' ? activeStyle : ''}" onclick="setBackupFilter('env','prod')">prod</button>`;
522
+ h += '</div>';
523
+
484524 // Local
485525 h += '<h2 style="font-size:1.125rem;font-weight:600;color:#f3f4f6;margin-bottom:0.75rem;">Local Backups</h2>';
486
- if (local.length === 0) {
487
- h += '<div class="card" style="color:#6b7280;">No local backups found.</div>';
526
+ if (filteredLocal.length === 0) {
527
+ h += '<div class="card" style="color:#6b7280;">No local backups match the current filter.</div>';
488528 } else {
489
- 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>';
490
- for (const b of local) {
491
- 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>`;
529
+ 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>';
530
+ for (const b of filteredLocal) {
531
+ h += `<tr>
532
+ <td>${esc(b.project||'')}</td>
533
+ <td><span class="badge badge-blue">${esc(b.env||b.environment||'')}</span></td>
534
+ <td class="mono" style="font-size:0.8125rem;">${esc(b.name||b.file||'')}</td>
535
+ <td>${esc(fmtBackupDate(b.date||b.timestamp||''))}</td>
536
+ <td>${esc(b.size_human||b.size||'')}</td>
537
+ </tr>`;
492538 }
493539 h += '</tbody></table></div>';
494540 }
495541
496542 // Offsite
497543 h += '<h2 style="font-size:1.125rem;font-weight:600;color:#f3f4f6;margin:1.5rem 0 0.75rem;">Offsite Backups</h2>';
498
- if (offsite.length === 0) {
499
- h += '<div class="card" style="color:#6b7280;">No offsite backups found.</div>';
544
+ if (filteredOffsite.length === 0) {
545
+ h += '<div class="card" style="color:#6b7280;">No offsite backups match the current filter.</div>';
500546 } else {
501
- 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>';
502
- for (const b of offsite) {
503
- 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>`;
547
+ 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>';
548
+ for (const b of filteredOffsite) {
549
+ h += `<tr>
550
+ <td>${esc(b.project||'')}</td>
551
+ <td><span class="badge badge-blue">${esc(b.env||b.environment||'')}</span></td>
552
+ <td class="mono" style="font-size:0.8125rem;">${esc(b.name||'')}</td>
553
+ <td>${esc(fmtBackupDate(b.date||''))}</td>
554
+ <td>${esc(b.size||'')}</td>
555
+ </tr>`;
504556 }
505557 h += '</tbody></table></div>';
506558 }
....@@ -704,6 +756,12 @@
704756 logCtx = { project: null, env: null, service: null };
705757 }
706758
759
+function setBackupFilter(type, value) {
760
+ if (type === 'project') backupFilterProject = value;
761
+ if (type === 'env') backupFilterEnv = value;
762
+ renderBackups();
763
+}
764
+
707765 async function createBackup(project, env) {
708766 if (!confirm(`Create backup for ${project}/${env}?`)) return;
709767 toast('Creating backup...', 'info');