Merge claude/1

This commit is contained in:
librelad 2026-07-16 17:28:07 +01:00
commit b94cda0ffe
2 changed files with 56 additions and 19 deletions

View File

@ -88,6 +88,35 @@
.updater-cve-pkg { color: rgba(var(--text-rgb), 0.6); }
.updater-cve-fix { margin-left: auto; color: rgba(var(--page-verify-rgb), 0.9); font-size: 0.76rem; }
/* Count pill next to the "Security" detail heading (e.g. "Security 28"). */
.updater-cve-count {
display: inline-block; vertical-align: middle; margin-left: 4px;
font-size: 0.68rem; font-weight: 700; line-height: 1; padding: 3px 8px;
border-radius: 999px;
background: rgba(var(--page-verify-rgb, var(--accent-rgb)), 0.16);
color: rgb(var(--page-verify-rgb, var(--accent-rgb)));
}
/* A long CVE list stays compact: once past a handful of rows it caps its height
and scrolls internally rather than pushing the whole page down. The first row
already carries a top border, so no seam is needed at the box edge. */
.updater-cve-scroll.is-scrollable {
max-height: 260px; overflow-y: auto;
padding-right: 8px; margin-right: -4px;
scrollbar-width: thin;
scrollbar-color: rgba(var(--text-rgb), 0.28) transparent;
/* Bottom-only fade hints there's more below; the top stays crisp so the
worst-severity rows (sorted first) are never dimmed. */
-webkit-mask-image: linear-gradient(to bottom, #000 calc(100% - 18px), transparent 100%);
mask-image: linear-gradient(to bottom, #000 calc(100% - 18px), transparent 100%);
}
.updater-cve-scroll.is-scrollable::-webkit-scrollbar { width: 8px; }
.updater-cve-scroll.is-scrollable::-webkit-scrollbar-track { background: transparent; }
.updater-cve-scroll.is-scrollable::-webkit-scrollbar-thumb {
background: rgba(var(--text-rgb), 0.22); border-radius: 999px;
}
.updater-cve-scroll.is-scrollable::-webkit-scrollbar-thumb:hover { background: rgba(var(--text-rgb), 0.34); }
/* ---- Buttons ---- */
.updater-btn {
display: inline-flex; align-items: center; gap: 6px;

View File

@ -183,6 +183,27 @@ class UpdaterPage {
return null;
}
sevRank(s) {
const r = { critical: 0, high: 1, medium: 2, low: 3 };
return r[(s || '').toLowerCase()] ?? 4;
}
// One CVE list, shared by the standalone Security tab and the per-app expander.
// Sorted worst-first (so the most severe are visible before any scroll) and
// wrapped in a height-capped scroll box once the list is long, so an app with
// dozens of CVEs (e.g. 28) stays compact instead of pushing the page down.
renderCveList(cves) {
const list = [...(cves || [])].sort((x, y) => this.sevRank(x.severity) - this.sevRank(y.severity));
const items = list.map(c => `
<div class="updater-cve sev-${(c.severity || 'low').toLowerCase()}">
<span class="updater-cve-sev">${this.escape((c.severity || '').toUpperCase())}</span>
<a class="updater-cve-id" href="${this.escape(c.url || ('https://nvd.nist.gov/vuln/detail/' + (c.id || '')))}" target="_blank" rel="noopener">${this.escape(c.id || 'CVE')}</a>
<span class="updater-cve-pkg">${this.escape(c.package || '')}</span>
${c.fixed_in ? `<span class="updater-cve-fix">fixed in ${this.escape(c.fixed_in)}</span>` : ''}
</div>`).join('');
return `<div class="updater-cve-scroll${list.length > 6 ? ' is-scrollable' : ''}">${items}</div>`;
}
// The CVE scanner's live state, stamped on cves.json by the updater generator:
// 'ready' — Trivy's vulnerability DB is present; results are real
// 'db_updating' — Trivy is installed but still downloading its DB (no
@ -417,16 +438,9 @@ class UpdaterPage {
// manual Check), so the message alone is the right button-free empty UI.
if (!this.cves) return this.empty('No vulnerability scan yet — one runs automatically within a couple of minutes.');
if (!withCves.length) return this.empty('No known vulnerabilities in your installed apps. 🎉');
const blocks = withCves.map(a => {
const items = (a.cves || []).map(c => `
<div class="updater-cve sev-${(c.severity || 'low').toLowerCase()}">
<span class="updater-cve-sev">${this.escape((c.severity || '').toUpperCase())}</span>
<a class="updater-cve-id" href="${this.escape(c.url || ('https://nvd.nist.gov/vuln/detail/' + (c.id || '')))}" target="_blank" rel="noopener">${this.escape(c.id || 'CVE')}</a>
<span class="updater-cve-pkg">${this.escape(c.package || '')}</span>
${c.fixed_in ? `<span class="updater-cve-fix">fixed in ${this.escape(c.fixed_in)}</span>` : ''}
</div>`).join('');
return `<div class="updater-cve-app"><div class="updater-cve-app-name">${this.escape(a.displayName)} <span class="updater-badge sev-${a.worstSeverity}">${(a.cves || []).length}</span></div>${items}</div>`;
}).join('');
const blocks = withCves.map(a =>
`<div class="updater-cve-app"><div class="updater-cve-app-name">${this.escape(a.displayName)} <span class="updater-badge sev-${a.worstSeverity}">${(a.cves || []).length}</span></div>${this.renderCveList(a.cves)}</div>`
).join('');
return `<div class="updater-list">${blocks}</div>`;
}
@ -480,15 +494,9 @@ class UpdaterPage {
<div class="updater-detail-row">${badge} <span class="updater-row-ver">${cur}${avail ? ` <span class="updater-arrow">→</span> <strong>${avail}</strong>` : ''}</span></div></div>`;
}
const cves = a.cves || [];
const cveItems = cves.map((c) => `
<div class="updater-cve sev-${(c.severity || 'low').toLowerCase()}">
<span class="updater-cve-sev">${this.escape((c.severity || '').toUpperCase())}</span>
<a class="updater-cve-id" href="${this.escape(c.url || ('https://nvd.nist.gov/vuln/detail/' + (c.id || '')))}" target="_blank" rel="noopener">${this.escape(c.id || 'CVE')}</a>
<span class="updater-cve-pkg">${this.escape(c.package || '')}</span>
${c.fixed_in ? `<span class="updater-cve-fix">fixed in ${this.escape(c.fixed_in)}</span>` : ''}
</div>`).join('');
const security = `<div class="updater-detail-section"><h4>Security</h4>${
cves.length ? cveItems : '<p class="updater-detail-empty">No known CVEs. 🎉</p>'}</div>`;
const security = `<div class="updater-detail-section"><h4>Security${
cves.length ? ` <span class="updater-cve-count">${cves.length}</span>` : ''}</h4>${
cves.length ? this.renderCveList(cves) : '<p class="updater-detail-empty">No known CVEs. 🎉</p>'}</div>`;
// A rollback target exists if a snapshot field is present (future-proofing)
// OR — the data the generator actually emits today — this app has a prior