From 117e49dfa033119c45fa9df1c7645feeb0c589c6 Mon Sep 17 00:00:00 2001 From: Tyler <53561637+im-tyler@users.noreply.github.com> Date: Wed, 11 Nov 2026 18:46:54 -0700 Subject: [PATCH] Home: count uncovered backups per server, drop duplicate backup incidents, cap the list Before backups are set up the list is one protect row; afterwards uncovered targets are one row per server. Backup incidents no longer repeat the backup rows. The list shows eight rows with the rest behind one button, and long details are clamped to two lines. Co-Authored-By: Claude Fable 5.2 --- .../frontend-test/backup_setup.test.mjs | 34 +++++++++++++++++ cmd/teploy-dash/frontend/css/style.css | 4 +- cmd/teploy-dash/frontend/index.html | 10 ++++- cmd/teploy-dash/frontend/js/app.js | 38 +++++++++++++++---- 4 files changed, 76 insertions(+), 10 deletions(-) diff --git a/cmd/teploy-dash/frontend-test/backup_setup.test.mjs b/cmd/teploy-dash/frontend-test/backup_setup.test.mjs index 504cd7f..7e28091 100644 --- a/cmd/teploy-dash/frontend-test/backup_setup.test.mjs +++ b/cmd/teploy-dash/frontend-test/backup_setup.test.mjs @@ -493,3 +493,37 @@ test('no emoji in the page, and the layout holds at phone width', () => { const css = readFileSync(join(root, 'css/style.css'), 'utf8'); assert.ok(css.includes('.setup-step input:not([type="checkbox"]):not([type="radio"]), .setup-step select { width: 100%; max-width: 100%; }')); }); + +test('the home list stays a glance: one row before setup, grouped after, capped', async () => { + const h = harness('/'); + const rows = []; + for (let i = 0; i < 12; i++) rows.push({ server: i < 9 ? 'a' : 'b', name: 'app' + i + '/volumes', state: 'uncovered' }); + rows.push({ server: 'a', name: 'ship/postgres', state: 'failing', last_error: 'upload failed' }); + const open = rows.map((r) => ({ key: 'backup:' + r.server + ':uncovered:' + r.name, source: 'backup', name: r.name + ' on ' + r.server + ' is not covered', severity: 'warning' })); + open.push({ key: 'm1', source: 'monitor', name: 'Shop', severity: 'critical' }); + let view = baseView({}, { rows }); + h.respond((url) => url === '/api/backups' ? response(view) : url === '/api/alerts/incidents' ? response({ open }) : response({})); + const home = h.components.homeStatus(); + await home.init(); + let keys = home.attention().map((r) => r.key); + assert.deepEqual(keys, ['inc:m1', 'protect', 'bk:a/ship/postgres'], 'before setup: the monitor, one protect row, and the failing backup'); + view = baseView({ state: 'partial', headline: 'One of two servers is protected.' }, { rows }); + await home.loadOne('backups'); + const att = home.attention(); + keys = att.map((r) => r.key); + assert.deepEqual(keys, ['inc:m1', 'protect', 'bk:a/ship/postgres', 'bk-uncovered:a', 'bk-uncovered:b'], 'after setup: uncovered targets are one row per server'); + assert.equal(att[3].title, '9 targets on a are not backed up'); + assert.equal(att[3].detail, 'app0/volumes, app1/volumes, app2/volumes and 6 more'); + // A failed backups read keeps the backup incidents, so nothing goes unsaid. + const fresh = h.components.homeStatus(); + h.respond((url) => url === '/api/backups' ? response({}, { status: 500 }) : url === '/api/alerts/incidents' ? response({ open }) : response({})); + await fresh.init(); + assert.equal(fresh.attention().length, open.length); + assert.equal(fresh.attentionShown().length, 8); + assert.equal(fresh.attentionHidden(), open.length - 8); + fresh.showAllAttention = true; + assert.equal(fresh.attentionShown().length, open.length); + home.destroy(); fresh.destroy(); + const html = h.readHTML(); + assert.ok(html.includes('attentionShown()') && html.includes("'Show ' + attentionHidden() + ' more'")); +}); diff --git a/cmd/teploy-dash/frontend/css/style.css b/cmd/teploy-dash/frontend/css/style.css index 73eab00..552ca81 100644 --- a/cmd/teploy-dash/frontend/css/style.css +++ b/cmd/teploy-dash/frontend/css/style.css @@ -236,7 +236,9 @@ tbody tr:hover { background: var(--bg-card-hover); } a.status-row:hover { background: var(--bg-card-hover); } .status-row-main { display: flex; min-width: 0; flex: 1; flex-direction: column; gap: 1px; } .status-row-title { font-size: 13px; overflow-wrap: anywhere; } -.status-row-detail { color: var(--text-muted); font-size: 12px; overflow-wrap: anywhere; } +.status-row-detail { display: -webkit-box; overflow: hidden; color: var(--text-muted); font-size: 12px; overflow-wrap: anywhere; -webkit-box-orient: vertical; -webkit-line-clamp: 2; line-clamp: 2; } +.status-row-more { width: 100%; border: 0; border-top: 1px solid var(--border); background: none; color: var(--text-muted); font: inherit; font-size: 13px; cursor: pointer; justify-content: center; } +.status-row-more:hover { background: var(--bg); color: var(--text); } .status-row-meta { flex-shrink: 0; color: var(--text-muted); font-size: 12px; white-space: nowrap; } .status-row-action { flex-shrink: 0; color: var(--accent); font-size: 12px; white-space: nowrap; } .status-row-facts { display: flex; flex-shrink: 0; gap: 14px; color: var(--text-secondary); font-size: 12px; white-space: nowrap; } diff --git a/cmd/teploy-dash/frontend/index.html b/cmd/teploy-dash/frontend/index.html index ad876af..7139cbc 100644 --- a/cmd/teploy-dash/frontend/index.html +++ b/cmd/teploy-dash/frontend/index.html @@ -162,17 +162,23 @@

Needs attention

-