| .. | .. |
|---|
| 18 | 18 | let refreshTimer = null; |
|---|
| 19 | 19 | const REFRESH_INTERVAL = 30000; |
|---|
| 20 | 20 | |
|---|
| 21 | +// Backup filter state |
|---|
| 22 | +let backupFilterProject = null; // null = all |
|---|
| 23 | +let backupFilterEnv = null; // null = all |
|---|
| 24 | + |
|---|
| 21 | 25 | // Log modal state |
|---|
| 22 | 26 | let logCtx = { project: null, env: null, service: null }; |
|---|
| 23 | 27 | |
|---|
| .. | .. |
|---|
| 459 | 463 | // --------------------------------------------------------------------------- |
|---|
| 460 | 464 | // Backups |
|---|
| 461 | 465 | // --------------------------------------------------------------------------- |
|---|
| 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 | + |
|---|
| 462 | 475 | async function renderBackups() { |
|---|
| 463 | 476 | updateBreadcrumbs(); |
|---|
| 464 | 477 | const c = document.getElementById('page-content'); |
|---|
| .. | .. |
|---|
| 467 | 480 | api('/api/backups/'), |
|---|
| 468 | 481 | api('/api/backups/offsite').catch(() => []), |
|---|
| 469 | 482 | ]); |
|---|
| 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 | + }); |
|---|
| 470 | 495 | |
|---|
| 471 | 496 | let h = '<div class="page-enter">'; |
|---|
| 472 | 497 | |
|---|
| .. | .. |
|---|
| 481 | 506 | } |
|---|
| 482 | 507 | h += '</div></div>'; |
|---|
| 483 | 508 | |
|---|
| 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 | + |
|---|
| 484 | 524 | // Local |
|---|
| 485 | 525 | 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>'; |
|---|
| 488 | 528 | } 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>`; |
|---|
| 492 | 538 | } |
|---|
| 493 | 539 | h += '</tbody></table></div>'; |
|---|
| 494 | 540 | } |
|---|
| 495 | 541 | |
|---|
| 496 | 542 | // Offsite |
|---|
| 497 | 543 | 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>'; |
|---|
| 500 | 546 | } 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>`; |
|---|
| 504 | 556 | } |
|---|
| 505 | 557 | h += '</tbody></table></div>'; |
|---|
| 506 | 558 | } |
|---|
| .. | .. |
|---|
| 704 | 756 | logCtx = { project: null, env: null, service: null }; |
|---|
| 705 | 757 | } |
|---|
| 706 | 758 | |
|---|
| 759 | +function setBackupFilter(type, value) { |
|---|
| 760 | + if (type === 'project') backupFilterProject = value; |
|---|
| 761 | + if (type === 'env') backupFilterEnv = value; |
|---|
| 762 | + renderBackups(); |
|---|
| 763 | +} |
|---|
| 764 | + |
|---|
| 707 | 765 | async function createBackup(project, env) { |
|---|
| 708 | 766 | if (!confirm(`Create backup for ${project}/${env}?`)) return; |
|---|
| 709 | 767 | toast('Creating backup...', 'info'); |
|---|