mirror of
https://tangled.org/evan.jarrett.net/at-container-registry
synced 2026-09-23 18:54:16 +00:00
Artifacts filter had no empty state: filtering to zero matches left a blank panel, indistinguishable from something being broken. It now uses the existing state-empty partial, with no CTA since the filter input is right there. Rows appended by Load More now also obey the active filter, which is a change to existing behaviour but is required for the empty state to be truthful. The tag icon beside the tag selector computed to 5.44px by 24px on every repo page: the class was right, the flex parent was shrinking it. Adds shrink-0 to that instance only. 31 other icons are direct flex children without shrink-0 and are left alone, since a site-wide sweep found only this one squeezed. Digest-page scan tabs set role="tab" but never aria-selected, while the repo-page tabs do. Both are now set server-side so first paint is correct, with JS keeping them in sync afterwards, following switchRepoTab's pattern. The diff-content tabs had the identical defect and are fixed too, since the handler is generic over radio tabs and leaving them out would have meant attributes set by JS but never by the server. Includes the bundle rebuild for these and the two /auth/token-adjacent JS changes, since nothing in the dev loop keeps that artifact current on its own. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PDqoCE1j3njokkZ9b1C5n9
376 lines
22 KiB
HTML
376 lines
22 KiB
HTML
{{ define "diff-content" }}
|
|
<!-- Layers + Vulnerabilities Diff -->
|
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
<!-- Layer Diff (Left) -->
|
|
<div class="card bg-base-200 shadow-sm border border-base-300 p-6 space-y-4 min-w-0">
|
|
<h2 class="text-lg font-semibold">Layers</h2>
|
|
{{ if .LayerDiff }}
|
|
<div class="overflow-x-auto">
|
|
<table class="table table-xs w-full">
|
|
<caption class="sr-only">Layer differences</caption>
|
|
<thead>
|
|
<tr class="text-xs">
|
|
<th scope="col" class="w-6"><span class="sr-only">Change</span></th>
|
|
<th scope="col" class="w-8">#</th>
|
|
<th scope="col">Command</th>
|
|
<th scope="col" class="text-right w-24">Size</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{{ range .LayerDiff }}
|
|
<tr class="{{ if eq .Status "added" }}bg-success/10{{ else if eq .Status "removed" }}bg-error/10{{ else if eq .Status "rebuilt" }}bg-warning/10{{ else }}opacity-60{{ end }}">
|
|
<td class="font-mono text-xs text-center font-bold {{ if eq .Status "added" }}text-success{{ else if eq .Status "removed" }}text-error{{ else if eq .Status "rebuilt" }}text-warning{{ end }}">{{/* Glyph + sr-only label: color is redundant information. */}}{{ if eq .Status "added" }}<span aria-hidden="true">+</span><span class="sr-only">Added</span>{{ else if eq .Status "removed" }}<span aria-hidden="true">-</span><span class="sr-only">Removed</span>{{ else if eq .Status "rebuilt" }}<span aria-hidden="true">~</span><span class="sr-only">Rebuilt</span>{{ else }}<span class="sr-only">Unchanged</span>{{ end }}</td>
|
|
<td class="font-mono text-xs">{{ .Layer.Index }}</td>
|
|
<td>
|
|
{{ if .Layer.Command }}
|
|
<code class="font-mono text-xs break-all line-clamp-2" title="{{ .Layer.Command }}">{{ .Layer.Command }}</code>
|
|
{{ end }}
|
|
</td>
|
|
<td class="text-right text-sm whitespace-nowrap">
|
|
{{ if not .Layer.EmptyLayer }}{{ humanizeBytes .Layer.Size }}{{ end }}
|
|
{{ if and (eq .Status "rebuilt") .PrevLayer }}
|
|
{{ if ne .Layer.Size .PrevLayer.Size }}
|
|
<span class="text-xs text-base-content/70">({{ humanizeByteDelta (sub64 .Layer.Size .PrevLayer.Size) }})</span>
|
|
{{ end }}
|
|
{{ end }}
|
|
</td>
|
|
</tr>
|
|
{{ end }}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{{ else }}
|
|
{{ template "state-empty" (dict
|
|
"Icon" "history"
|
|
"Title" "No layer history"
|
|
"Subtext" "Neither side of this diff reports layer history."
|
|
) }}
|
|
{{ end }}
|
|
</div>
|
|
|
|
<!-- Vulnerability + Package Diff (Right) -->
|
|
<div class="card bg-base-200 shadow-sm border border-base-300 p-6 min-w-0">
|
|
<div role="tablist" class="tabs tabs-bordered">
|
|
<input type="radio" id="diff-tab-vulns" name="diff-scan-tabs" role="tab" class="tab" aria-label="Vulnerabilities" aria-controls="diff-panel-vulns" aria-selected="true" checked="checked" />
|
|
<div id="diff-panel-vulns" role="tabpanel" aria-labelledby="diff-tab-vulns" class="tab-content pt-4 space-y-4">
|
|
|
|
{{ if not .HasVulnData }}
|
|
{{/* Branch on per-side scan status so users can tell "not scanned
|
|
yet" from "hold offline" from transient errors. */}}
|
|
{{ if or (eq .FromScanStatus "hold-unreachable") (eq .ToScanStatus "hold-unreachable") }}
|
|
<div class="alert alert-warning" role="status">
|
|
{{ icon "wifi-off" "size-4 shrink-0" }}
|
|
<span>We couldn't reach the hold to fetch scan data. Try again in a moment.</span>
|
|
</div>
|
|
{{ else if or (eq .FromScanStatus "no-data") (eq .ToScanStatus "no-data") }}
|
|
<p class="text-base-content/60">Neither manifest has been scanned yet. Vulnerability comparison will appear after both scans complete.</p>
|
|
{{ else }}
|
|
<p class="text-base-content/60">Vulnerability scan data isn't available for both manifests.</p>
|
|
{{ end }}
|
|
{{ else }}
|
|
|
|
<!-- Fixed Vulns -->
|
|
{{ if .FixedVulns }}
|
|
<div class="collapse collapse-arrow bg-success/5 border border-success/20 rounded-lg">
|
|
<input type="checkbox" checked aria-label="Toggle fixed vulnerabilities ({{ len .FixedVulns }})" />
|
|
<div class="collapse-title font-medium text-sm flex items-center gap-2">
|
|
{{ icon "shield-check" "size-4 text-success" }}
|
|
Fixed ({{ len .FixedVulns }})
|
|
</div>
|
|
<div class="collapse-content">
|
|
<div class="overflow-x-auto">
|
|
<table class="table table-xs w-full">
|
|
<caption class="sr-only">Vulnerabilities fixed in the newer manifest</caption>
|
|
<thead>
|
|
<tr class="text-xs">
|
|
<th scope="col">CVE</th>
|
|
<th scope="col">Severity</th>
|
|
<th scope="col">Package</th>
|
|
<th scope="col">Was</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{{ range .FixedVulns }}
|
|
<tr>
|
|
<td>
|
|
{{ if .CVEURL }}<a href="{{ .CVEURL }}" target="_blank" rel="noopener noreferrer" class="link link-primary text-xs font-mono">{{ or .CVEID "—" }}</a>
|
|
{{ else }}<span class="text-xs font-mono">{{ or .CVEID "—" }}</span>{{ end }}
|
|
</td>
|
|
<td>
|
|
<span class="badge badge-xs {{ if eq .Severity "Critical" }}badge-error{{ else if eq .Severity "High" }}badge-warning{{ else if eq .Severity "Medium" }}badge-info{{ else }}badge-ghost{{ end }}" title="{{ severityLabel .Severity }}">{{ severityLabel .Severity }}</span>
|
|
</td>
|
|
<td class="text-xs truncate max-w-xs" title="{{ .Package }}">{{ .Package }}</td>
|
|
<td class="text-xs font-mono truncate max-w-40" title="{{ .Version }}">{{ .Version }}</td>
|
|
</tr>
|
|
{{ end }}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{{ end }}
|
|
|
|
<!-- New Vulns -->
|
|
{{ if .NewVulns }}
|
|
<div class="collapse collapse-arrow bg-error/5 border border-error/20 rounded-lg">
|
|
<input type="checkbox" checked aria-label="Toggle new vulnerabilities ({{ len .NewVulns }})" />
|
|
<div class="collapse-title font-medium text-sm flex items-center gap-2">
|
|
{{ icon "alert-triangle" "size-4 text-error" }}
|
|
New ({{ len .NewVulns }})
|
|
</div>
|
|
<div class="collapse-content">
|
|
<div class="overflow-x-auto">
|
|
<table class="table table-xs w-full">
|
|
<caption class="sr-only">Vulnerabilities new to the newer manifest</caption>
|
|
<thead>
|
|
<tr class="text-xs">
|
|
<th scope="col">CVE</th>
|
|
<th scope="col">Severity</th>
|
|
<th scope="col">Package</th>
|
|
<th scope="col">Version</th>
|
|
<th scope="col">Fix</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{{ range .NewVulns }}
|
|
<tr>
|
|
<td>
|
|
{{ if .CVEURL }}<a href="{{ .CVEURL }}" target="_blank" rel="noopener noreferrer" class="link link-primary text-xs font-mono">{{ or .CVEID "—" }}</a>
|
|
{{ else }}<span class="text-xs font-mono">{{ or .CVEID "—" }}</span>{{ end }}
|
|
</td>
|
|
<td>
|
|
<span class="badge badge-xs {{ if eq .Severity "Critical" }}badge-error{{ else if eq .Severity "High" }}badge-warning{{ else if eq .Severity "Medium" }}badge-info{{ else }}badge-ghost{{ end }}" title="{{ severityLabel .Severity }}">{{ severityLabel .Severity }}</span>
|
|
</td>
|
|
<td class="text-xs truncate max-w-xs" title="{{ .Package }}">{{ .Package }}</td>
|
|
<td class="text-xs font-mono truncate max-w-40" title="{{ .Version }}">{{ .Version }}</td>
|
|
<td class="text-xs font-mono">{{ .FixedIn }}</td>
|
|
</tr>
|
|
{{ end }}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{{ end }}
|
|
|
|
<!-- Unchanged Vulns -->
|
|
{{ if .UnchangedVulns }}
|
|
<div class="collapse collapse-arrow bg-base-200/50 border border-base-300 rounded-lg">
|
|
<input type="checkbox" aria-label="Toggle unchanged vulnerabilities ({{ len .UnchangedVulns }})" />
|
|
<div class="collapse-title font-medium text-sm text-base-content/60">
|
|
Unchanged ({{ len .UnchangedVulns }})
|
|
</div>
|
|
<div class="collapse-content">
|
|
<div class="overflow-x-auto">
|
|
<table class="table table-xs w-full">
|
|
<caption class="sr-only">Vulnerabilities present in both manifests</caption>
|
|
<thead>
|
|
<tr class="text-xs">
|
|
<th scope="col">CVE</th>
|
|
<th scope="col">Severity</th>
|
|
<th scope="col">Package</th>
|
|
<th scope="col">Version</th>
|
|
<th scope="col">Fix</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{{ range .UnchangedVulns }}
|
|
<tr>
|
|
<td>
|
|
{{ if .CVEURL }}<a href="{{ .CVEURL }}" target="_blank" rel="noopener noreferrer" class="link link-primary text-xs font-mono">{{ or .CVEID "—" }}</a>
|
|
{{ else }}<span class="text-xs font-mono">{{ or .CVEID "—" }}</span>{{ end }}
|
|
</td>
|
|
<td>
|
|
<span class="badge badge-xs {{ if eq .Severity "Critical" }}badge-error{{ else if eq .Severity "High" }}badge-warning{{ else if eq .Severity "Medium" }}badge-info{{ else }}badge-ghost{{ end }}" title="{{ severityLabel .Severity }}">{{ severityLabel .Severity }}</span>
|
|
</td>
|
|
<td class="text-xs truncate max-w-xs" title="{{ .Package }}">{{ .Package }}</td>
|
|
<td class="text-xs font-mono truncate max-w-40" title="{{ .Version }}">{{ .Version }}</td>
|
|
<td class="text-xs font-mono">{{ .FixedIn }}</td>
|
|
</tr>
|
|
{{ end }}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{{ end }}
|
|
|
|
{{ if and (not .FixedVulns) (not .NewVulns) (not .UnchangedVulns) }}
|
|
<p class="text-base-content/60">No vulnerabilities found in either manifest</p>
|
|
{{ end }}
|
|
|
|
{{ end }}
|
|
</div>
|
|
|
|
<input type="radio" id="diff-tab-sbom" name="diff-scan-tabs" role="tab" class="tab" aria-label="Packages" aria-controls="diff-panel-sbom" aria-selected="false" />
|
|
<div id="diff-panel-sbom" role="tabpanel" aria-labelledby="diff-tab-sbom" class="tab-content pt-4 space-y-4">
|
|
|
|
{{ if not .HasSbomData }}
|
|
{{/* Mirror the vuln branching so users can tell "not scanned yet"
|
|
from "hold offline" from "this artifact type isn't scanned". */}}
|
|
{{ if or (eq .FromSbomStatus "hold-unreachable") (eq .ToSbomStatus "hold-unreachable") }}
|
|
<div class="alert alert-warning" role="status">
|
|
{{ icon "wifi-off" "size-4 shrink-0" }}
|
|
<span>We couldn't reach the hold to fetch SBOM data. Try again in a moment.</span>
|
|
</div>
|
|
{{ else if or (eq .FromSbomStatus "not-applicable") (eq .ToSbomStatus "not-applicable") }}
|
|
<p class="text-base-content/60">SBOMs aren't generated for this artifact type.</p>
|
|
{{ else if or (eq .FromSbomStatus "no-data") (eq .ToSbomStatus "no-data") }}
|
|
<p class="text-base-content/60">An SBOM isn't available for both manifests yet. Package comparison will appear after both scans complete.</p>
|
|
{{ else }}
|
|
<p class="text-base-content/60">SBOM data isn't available for both manifests.</p>
|
|
{{ end }}
|
|
{{ else }}
|
|
|
|
<!-- Changed Packages -->
|
|
{{ if .ChangedPackages }}
|
|
<div class="collapse collapse-arrow bg-warning/5 border border-warning/20 rounded-lg">
|
|
<input type="checkbox" checked aria-label="Toggle changed packages ({{ len .ChangedPackages }})" />
|
|
<div class="collapse-title font-medium text-sm flex items-center gap-2">
|
|
{{ icon "package" "size-4 text-warning" }}
|
|
Changed ({{ len .ChangedPackages }})
|
|
</div>
|
|
<div class="collapse-content">
|
|
<div class="overflow-x-auto">
|
|
<table class="table table-xs w-full">
|
|
<caption class="sr-only">Packages whose version changed between manifests</caption>
|
|
<thead>
|
|
<tr class="text-xs">
|
|
<th scope="col">Package</th>
|
|
<th scope="col">Type</th>
|
|
<th scope="col">Version</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{{ range .ChangedPackages }}
|
|
<tr>
|
|
<td class="text-xs truncate max-w-xs" title="{{ .Package.Name }}">{{ if .Package.URL }}<a href="{{ .Package.URL }}" target="_blank" rel="noopener noreferrer" class="link link-primary">{{ .Package.Name }}</a>{{ else }}{{ .Package.Name }}{{ end }}</td>
|
|
<td>{{ if .Package.Type }}<span class="badge badge-xs badge-ghost">{{ .Package.Type }}</span>{{ end }}</td>
|
|
<td class="text-xs font-mono truncate max-w-48" title="{{ .PrevVersion }} to {{ .Package.Version }}">{{ .PrevVersion }} <span aria-hidden="true">→</span><span class="sr-only">to</span> {{ .Package.Version }}</td>
|
|
</tr>
|
|
{{ end }}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{{ end }}
|
|
|
|
<!-- Added Packages -->
|
|
{{ if .AddedPackages }}
|
|
<div class="collapse collapse-arrow bg-success/5 border border-success/20 rounded-lg">
|
|
<input type="checkbox" checked aria-label="Toggle added packages ({{ len .AddedPackages }})" />
|
|
<div class="collapse-title font-medium text-sm flex items-center gap-2">
|
|
{{ icon "package-plus" "size-4 text-success" }}
|
|
Added ({{ len .AddedPackages }})
|
|
</div>
|
|
<div class="collapse-content">
|
|
<div class="overflow-x-auto">
|
|
<table class="table table-xs w-full">
|
|
<caption class="sr-only">Packages only present in the newer manifest</caption>
|
|
<thead>
|
|
<tr class="text-xs">
|
|
<th scope="col">Package</th>
|
|
<th scope="col">Version</th>
|
|
<th scope="col">Type</th>
|
|
<th scope="col">License</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{{ range .AddedPackages }}
|
|
<tr>
|
|
<td class="text-xs truncate max-w-xs" title="{{ .Package.Name }}">{{ if .Package.URL }}<a href="{{ .Package.URL }}" target="_blank" rel="noopener noreferrer" class="link link-primary">{{ .Package.Name }}</a>{{ else }}{{ .Package.Name }}{{ end }}</td>
|
|
<td class="text-xs font-mono truncate max-w-40" title="{{ .Package.Version }}">{{ .Package.Version }}</td>
|
|
<td>{{ if .Package.Type }}<span class="badge badge-xs badge-ghost">{{ .Package.Type }}</span>{{ end }}</td>
|
|
<td class="text-xs truncate max-w-40" title="{{ .Package.License }}">{{ .Package.License }}</td>
|
|
</tr>
|
|
{{ end }}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{{ end }}
|
|
|
|
<!-- Removed Packages -->
|
|
{{ if .RemovedPackages }}
|
|
<div class="collapse collapse-arrow bg-error/5 border border-error/20 rounded-lg">
|
|
<input type="checkbox" checked aria-label="Toggle removed packages ({{ len .RemovedPackages }})" />
|
|
<div class="collapse-title font-medium text-sm flex items-center gap-2">
|
|
{{ icon "package-minus" "size-4 text-error" }}
|
|
Removed ({{ len .RemovedPackages }})
|
|
</div>
|
|
<div class="collapse-content">
|
|
<div class="overflow-x-auto">
|
|
<table class="table table-xs w-full">
|
|
<caption class="sr-only">Packages only present in the older manifest</caption>
|
|
<thead>
|
|
<tr class="text-xs">
|
|
<th scope="col">Package</th>
|
|
<th scope="col">Version</th>
|
|
<th scope="col">Type</th>
|
|
<th scope="col">License</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{{ range .RemovedPackages }}
|
|
<tr>
|
|
<td class="text-xs truncate max-w-xs" title="{{ .Package.Name }}">{{ if .Package.URL }}<a href="{{ .Package.URL }}" target="_blank" rel="noopener noreferrer" class="link link-primary">{{ .Package.Name }}</a>{{ else }}{{ .Package.Name }}{{ end }}</td>
|
|
<td class="text-xs font-mono truncate max-w-40" title="{{ .Package.Version }}">{{ .Package.Version }}</td>
|
|
<td>{{ if .Package.Type }}<span class="badge badge-xs badge-ghost">{{ .Package.Type }}</span>{{ end }}</td>
|
|
<td class="text-xs truncate max-w-40" title="{{ .Package.License }}">{{ .Package.License }}</td>
|
|
</tr>
|
|
{{ end }}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{{ end }}
|
|
|
|
<!-- Unchanged Packages -->
|
|
{{ if .UnchangedPackages }}
|
|
<div class="collapse collapse-arrow bg-base-200/50 border border-base-300 rounded-lg">
|
|
<input type="checkbox" aria-label="Toggle unchanged packages ({{ len .UnchangedPackages }})" />
|
|
<div class="collapse-title font-medium text-sm text-base-content/60">
|
|
Unchanged ({{ len .UnchangedPackages }})
|
|
</div>
|
|
<div class="collapse-content">
|
|
<div class="overflow-x-auto overflow-y-auto max-h-128">
|
|
<table class="table table-xs w-full">
|
|
<caption class="sr-only">Packages identical in both manifests</caption>
|
|
<thead>
|
|
<tr class="text-xs">
|
|
<th scope="col">Package</th>
|
|
<th scope="col">Version</th>
|
|
<th scope="col">Type</th>
|
|
<th scope="col">License</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{{ range .UnchangedPackages }}
|
|
<tr>
|
|
<td class="text-xs truncate max-w-xs" title="{{ .Package.Name }}">{{ if .Package.URL }}<a href="{{ .Package.URL }}" target="_blank" rel="noopener noreferrer" class="link link-primary">{{ .Package.Name }}</a>{{ else }}{{ .Package.Name }}{{ end }}</td>
|
|
<td class="text-xs font-mono truncate max-w-40" title="{{ .Package.Version }}">{{ .Package.Version }}</td>
|
|
<td>{{ if .Package.Type }}<span class="badge badge-xs badge-ghost">{{ .Package.Type }}</span>{{ end }}</td>
|
|
<td class="text-xs truncate max-w-40" title="{{ .Package.License }}">{{ .Package.License }}</td>
|
|
</tr>
|
|
{{ end }}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{{ end }}
|
|
|
|
{{ if and (not .ChangedPackages) (not .AddedPackages) (not .RemovedPackages) (not .UnchangedPackages) }}
|
|
<p class="text-base-content/60">No packages found in either manifest</p>
|
|
{{ end }}
|
|
|
|
{{ end }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{{ end }}
|