From c1349db5618e97b857b173ffd38b5ded034a5036 Mon Sep 17 00:00:00 2001 From: Omar Rao Date: Wed, 26 Aug 2026 14:00:42 -0400 Subject: [PATCH] @ fix(dashboard): interactive health tiles + storage-chart tooltips; remove tile checkmark MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - System Health tiles are now buttons (click/keyboard) that navigate to their section (Runs/Reports/Restore/Settings), with hover, focus ring, and arrow. - Storage Growth points show a rich hover/focus tooltip (session name, size, delta mode, date) and enlarge the active point; larger hit targets + a11y. - Removed the ✅ glyph from the Last Backup stat card (demo mode). - Docs (USERGUIDE 15A) + dashboard-insights.svg updated; SW cache v7; v5.0.2. Verified in browser (demo): no checkmark, 8 interactive tiles routing correctly, 10 chart points with tooltip, tile click navigates. 105 tests pass, lint clean. @ --- CHANGELOG.md | 14 ++++ docs/USERGUIDE.md | 24 ++++--- docs/index.html | 93 +++++++++++++++++++++---- docs/screenshots/dashboard-insights.svg | 12 +++- docs/sw.js | 2 +- package-lock.json | 4 +- package.json | 2 +- 7 files changed, 121 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d7e6885..663ad0f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 --- +## [5.0.2] — 2026-07-20 + +### Changed (dashboard UX) +- **System Health tiles are now interactive** — each tile is a button (click or + keyboard) that navigates to its section (Last Backup→Runs, SLA/Anomaly→Reports, + Restore Verified→Restore, Signature/Storage/GitHub/Drive→Settings), with hover + and focus states and a hover arrow. +- **Storage Growth chart points are interactive** — hovering/focusing a point + shows a rich tooltip (session name, size, delta mode, date) and enlarges the + active point. +- **Removed the ✅ glyph** from the Last Backup stat card (demo mode). + +--- + ## [5.0.1] — 2026-07-20 ### Changed diff --git a/docs/USERGUIDE.md b/docs/USERGUIDE.md index 032cddc..e16304f 100644 --- a/docs/USERGUIDE.md +++ b/docs/USERGUIDE.md @@ -948,20 +948,22 @@ Version 3.1 adds a layer of at-a-glance intelligence and keyboard-first navigati ### System Health panel -Directly under the stat cards, a six-tile **System Health** grid gives a traffic-light read on the whole system: - -| Tile | Green | Amber | Red | -|---|---|---|---| -| Last Backup | Success | — | Failed | -| SLA | Within threshold | — | Breached | -| Anomaly | Normal | Detected (>20% size deviation) | — | -| Storage | ≤80% used | 80–90% | >90% | -| GitHub | Connected | — | — | -| Drive | Connected | — | — | +Directly under the stat cards, the **System Health** grid gives a traffic-light read on the whole system. **Every tile is interactive** — click (or focus + Enter) any tile to jump straight to the section that explains or acts on it: + +| Tile | Green | Amber | Red | Opens | +|---|---|---|---|---| +| Last Backup | Success | — | Failed | Workflow Runs | +| SLA | Within threshold | — | Breached | Reports | +| Anomaly | Normal | Detected (>20% size deviation) | — | Reports | +| Restore Verified | Verified (with RTO) | — | Failed | Restore | +| Manifest Signature | Signed | — | — | Settings | +| Storage | ≤80% used | 80–90% | >90% | Settings | +| GitHub | Connected | — | — | Settings | +| Drive | Connected | — | — | Settings | ### Storage Growth chart -An inline SVG area chart plots the size (MB) of the last 10 backup sessions oldest→newest, with hover tooltips per point. Use it to spot steady growth (plan capacity) or sudden drops (investigate with Session Diff). +An inline SVG area chart plots the size (MB) of the last 10 backup sessions oldest→newest. **Hover (or keyboard-focus) any point** to see an interactive tooltip with that session's name, size, delta mode (full/delta/unchanged), and date; the point enlarges to mark the active selection. Use it to spot steady growth (plan capacity) or sudden drops (investigate with Session Diff). ### Backup Timeline heatmap diff --git a/docs/index.html b/docs/index.html index 1a6e161..6302d82 100644 --- a/docs/index.html +++ b/docs/index.html @@ -1027,23 +1027,50 @@ display: flex; align-items: center; gap: 11px; padding: 12px 14px; border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--bg-hover); + cursor: pointer; user-select: none; + transition: border-color var(--t), box-shadow var(--t), transform var(--t), background var(--t); + } + .health-item:hover { + border-color: var(--accent); background: var(--bg-card); + box-shadow: var(--shadow-md); transform: translateY(-1px); } + .health-item:hover .health-arrow { opacity: 1; transform: translateX(0); color: var(--accent); } + .health-item:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; } .health-led { width: 12px; height: 12px; border-radius: 50%; flex-shrink: 0; box-shadow: 0 0 0 3px transparent; } .health-led.ok { background: var(--primary); box-shadow: 0 0 0 3px var(--primary-bg); } .health-led.warn { background: var(--warn); box-shadow: 0 0 0 3px var(--warn-bg); } .health-led.err { background: var(--danger); box-shadow: 0 0 0 3px var(--danger-bg); } .health-led.idle { background: var(--muted-light); } - .health-meta { display: flex; flex-direction: column; min-width: 0; } + .health-meta { display: flex; flex-direction: column; min-width: 0; flex: 1; } .health-name { font-size: 11px; color: var(--muted); font-weight: 600; text-transform: uppercase; letter-spacing: .4px; } .health-val { font-size: 13.5px; font-weight: 700; color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } + .health-arrow { width: 15px; height: 15px; flex-shrink: 0; color: var(--muted-light); opacity: 0; transform: translateX(-3px); transition: opacity var(--t), transform var(--t), color var(--t); } /* ── Storage growth area chart ────────────────────────────────────────── */ .area-chart { width: 100%; height: 140px; display: block; } .area-chart .grid-line { stroke: var(--border); stroke-width: 1; } .area-chart .area-fill { fill: var(--accent); opacity: .12; } .area-chart .area-line { fill: none; stroke: var(--accent); stroke-width: 2; stroke-linejoin: round; stroke-linecap: round; } - .area-chart .area-dot { fill: var(--bg-card); stroke: var(--accent); stroke-width: 2; } + .area-chart .area-dot { fill: var(--bg-card); stroke: var(--accent); stroke-width: 2; transition: r .12s ease; } + .area-chart .area-dot.active { r: 5; fill: var(--accent); } + .area-chart .area-hit { fill: transparent; cursor: pointer; outline: none; } + .area-chart .area-hit:focus-visible + text, .area-chart .area-hit:focus-visible { stroke: var(--accent); } .area-chart text { fill: var(--muted); font-size: 10px; } + /* Interactive chart tooltip */ + .chart-tip { + position: absolute; z-index: 5; transform: translate(-50%, calc(-100% - 12px)); + background: var(--bg-sidebar); color: #e2e8f0; border: 1px solid var(--border-mid); + border-radius: var(--radius-sm); padding: 8px 10px; font-size: 11.5px; line-height: 1.5; + pointer-events: none; box-shadow: var(--shadow-lg); white-space: nowrap; min-width: 90px; + } + .chart-tip[hidden] { display: none; } + .chart-tip .tip-title { font-weight: 700; color: #fff; font-size: 12px; margin-bottom: 2px; } + .chart-tip .tip-row { display: flex; align-items: center; gap: 8px; } + .chart-tip .tip-sub { color: #94a3b8; font-size: 10.5px; margin-top: 2px; } + .chart-tip .tip-mode { font-size: 9.5px; font-weight: 700; text-transform: uppercase; letter-spacing: .3px; padding: 1px 6px; border-radius: 10px; } + .chart-tip .tip-mode.full { background: #2563eb; color: #fff; } + .chart-tip .tip-mode.delta { background: #10b981; color: #062e1f; } + .chart-tip .tip-mode.unchanged { background: #475569; color: #e2e8f0; } /* ── Timeline heatmap ─────────────────────────────────────────────────── */ .heatmap { display: flex; flex-wrap: wrap; gap: 4px; } @@ -2888,7 +2915,7 @@

Recent Runs

document.getElementById('dash-warn').classList.add('hidden'); document.getElementById('stat-repos').textContent = DEMO.stats.repos; document.getElementById('stat-sessions').textContent = DEMO.stats.sessions; - document.getElementById('stat-last').textContent = relTime(DEMO.stats.lastBackup) + ' ✅'; + document.getElementById('stat-last').textContent = relTime(DEMO.stats.lastBackup); document.getElementById('stat-active').textContent = DEMO.stats.active; setTrend('trend-repos', '↑ +1 vs last week', 'up'); setTrend('trend-sessions', '↑ +12% vs last week', 'up'); @@ -3072,23 +3099,28 @@

Recent Runs

const rec = demo ? DEMO.recovery : window._recovery; const sig = demo ? DEMO.signature : (_latestSummary && _latestSummary.signature); + // Each tile links to the section that explains or acts on it. const items = [ - { name: 'Last Backup', val: lastOk == null ? 'No data' : (lastOk ? 'Success' : 'Failed'), led: lastOk == null ? 'idle' : (lastOk ? 'ok' : 'err') }, - { name: 'SLA', val: slaOk == null ? '—' : (slaOk ? `Within ${slaHours}h` : 'Breached'), led: slaOk == null ? 'idle' : (slaOk ? 'ok' : 'err') }, - { name: 'Anomaly', val: anomaly == null ? 'No data' : (anomaly ? 'Detected' : 'Normal'), led: anomaly == null ? 'idle' : (anomaly ? 'warn' : 'ok') }, - { name: 'Restore Verified', val: !rec || rec.status === 'pending' ? 'No drill yet' : (rec.status === 'verified' ? `${Math.round(rec.rto_seconds/60*10)/10}m RTO` : 'Failed'), led: !rec || rec.status === 'pending' ? 'idle' : (rec.status === 'verified' ? 'ok' : 'err') }, - { name: 'Manifest Signature', val: !sig ? 'Unsigned' : `Signed (${sig.key_fingerprint ? sig.key_fingerprint.slice(0,8) : sig.algorithm || 'ed25519'})`, led: sig ? 'ok' : 'idle' }, - { name: 'Storage', val: quotaPct == null ? '—' : `${quotaPct}% used`, led: quotaPct == null ? 'idle' : (quotaPct > 90 ? 'err' : quotaPct > 80 ? 'warn' : 'ok') }, - { name: 'GitHub', val: (demo || ghToken) ? 'Connected' : 'Not linked', led: (demo || ghToken) ? 'ok' : 'idle' }, - { name: 'Drive', val: (demo || driveToken) ? 'Connected' : 'Not linked', led: (demo || driveToken) ? 'ok' : 'idle' }, + { name: 'Last Backup', val: lastOk == null ? 'No data' : (lastOk ? 'Success' : 'Failed'), led: lastOk == null ? 'idle' : (lastOk ? 'ok' : 'err'), page: 'runs' }, + { name: 'SLA', val: slaOk == null ? '—' : (slaOk ? `Within ${slaHours}h` : 'Breached'), led: slaOk == null ? 'idle' : (slaOk ? 'ok' : 'err'), page: 'reports' }, + { name: 'Anomaly', val: anomaly == null ? 'No data' : (anomaly ? 'Detected' : 'Normal'), led: anomaly == null ? 'idle' : (anomaly ? 'warn' : 'ok'), page: 'reports' }, + { name: 'Restore Verified', val: !rec || rec.status === 'pending' ? 'No drill yet' : (rec.status === 'verified' ? `${Math.round(rec.rto_seconds/60*10)/10}m RTO` : 'Failed'), led: !rec || rec.status === 'pending' ? 'idle' : (rec.status === 'verified' ? 'ok' : 'err'), page: 'restore' }, + { name: 'Manifest Signature', val: !sig ? 'Unsigned' : `Signed (${sig.key_fingerprint ? sig.key_fingerprint.slice(0,8) : sig.algorithm || 'ed25519'})`, led: sig ? 'ok' : 'idle', page: 'settings' }, + { name: 'Storage', val: quotaPct == null ? '—' : `${quotaPct}% used`, led: quotaPct == null ? 'idle' : (quotaPct > 90 ? 'err' : quotaPct > 80 ? 'warn' : 'ok'), page: 'settings' }, + { name: 'GitHub', val: (demo || ghToken) ? 'Connected' : 'Not linked', led: (demo || ghToken) ? 'ok' : 'idle', page: 'settings' }, + { name: 'Drive', val: (demo || driveToken) ? 'Connected' : 'Not linked', led: (demo || driveToken) ? 'ok' : 'idle', page: 'settings' }, ]; + const cap = s => s.charAt(0).toUpperCase() + s.slice(1); el.innerHTML = items.map(i => ` -
+
${i.name} ${i.val} +
`).join(''); } @@ -3106,8 +3138,41 @@

Recent Runs

const line = pts.map((p, i) => (i ? 'L' : 'M') + p[0].toFixed(1) + ' ' + p[1].toFixed(1)).join(' '); const area = `M${pts[0][0].toFixed(1)} ${(padT+ih).toFixed(1)} ` + pts.map(p => 'L'+p[0].toFixed(1)+' '+p[1].toFixed(1)).join(' ') + ` L${pts[pts.length-1][0].toFixed(1)} ${(padT+ih).toFixed(1)} Z`; const gridY = [0, 0.5, 1].map(f => { const gy = padT + ih - ih*f; return `${Math.round(max*f)}`; }).join(''); - const dots = pts.map((p,i) => `${data[i].name}: ${data[i].sizeMB} MB`).join(''); - el.innerHTML = `${gridY}${dots}oldestnewest · MB`; + // Visible dot + a larger transparent hit target for easy hover/focus. + const dots = pts.map((p, i) => + `` + + `` + ).join(''); + el.style.position = 'relative'; + el.innerHTML = + `` + + `${gridY}${dots}` + + `oldestnewest · MB` + + ``; + + const tip = el.querySelector('#storage-tip'); + const show = (i) => { + const d = data[i]; + const when = d.createdTime ? relTime(d.createdTime) : ''; + const mode = d.mode ? `${d.mode}` : ''; + tip.innerHTML = `
${d.name}
` + + `
${d.sizeMB} MB${mode}
` + + (when ? `
${when}
` : ''); + // viewBox maps linearly to the element box (preserveAspectRatio=none), + // so fractional coordinates position the tooltip exactly. + tip.style.left = (pts[i][0] / W * 100) + '%'; + tip.style.top = (pts[i][1] / H * 100) + '%'; + tip.hidden = false; + el.querySelectorAll('.area-dot').forEach((c, j) => c.classList.toggle('active', j === i)); + }; + const hide = () => { tip.hidden = true; el.querySelectorAll('.area-dot.active').forEach(c => c.classList.remove('active')); }; + el.querySelectorAll('.area-hit').forEach(h => { + const i = +h.dataset.i; + h.addEventListener('mouseenter', () => show(i)); + h.addEventListener('mouseleave', hide); + h.addEventListener('focus', () => show(i)); + h.addEventListener('blur', hide); + }); } function renderHeatmap(sessions, runs) { diff --git a/docs/screenshots/dashboard-insights.svg b/docs/screenshots/dashboard-insights.svg index 9ff2a96..c6fbd39 100644 --- a/docs/screenshots/dashboard-insights.svg +++ b/docs/screenshots/dashboard-insights.svg @@ -56,7 +56,17 @@ - + + + + + + + session-2026-08-20 + 488 MB + + DELTA + 8/20/2026 oldest newest · MB diff --git a/docs/sw.js b/docs/sw.js index 75c254f..b6fb8c6 100644 --- a/docs/sw.js +++ b/docs/sw.js @@ -3,7 +3,7 @@ // This file is available under the GNU Affero General Public License v3.0 // or under a separate commercial license. // Service Worker — cache-first for the app shell -const CACHE = 'gh-backup-v6'; +const CACHE = 'gh-backup-v7'; const SHELL = [ '/github-gdrive-backup/', '/github-gdrive-backup/index.html', diff --git a/package-lock.json b/package-lock.json index ae8c186..0f31200 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "github-gdrive-backup", - "version": "5.0.1", + "version": "5.0.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "github-gdrive-backup", - "version": "5.0.1", + "version": "5.0.2", "license": "AGPL-3.0-only", "dependencies": { "@aws-sdk/client-s3": "^3.600.0", diff --git a/package.json b/package.json index 97b874a..417e1a9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "github-gdrive-backup", - "version": "5.0.1", + "version": "5.0.2", "description": "Back up every GitHub repository to Google Drive — code, issues, PRs, releases, wiki, labels, milestones — and restore with one click.", "main": "src/server/app.js", "bin": {