build(master): vendor Simple Icons data and the Ledger fonts

Icons: simple-icons@16.35.0 (CC0), generated by
go run ./internal/master/web/icons/gen, which downloads and verifies:
https://registry.npmjs.org/simple-icons/-/simple-icons-16.35.0.tgz
20ebead4f6bc93464812a5e260d19fc4d84772e5eeff69a6fbea263ca2441c5f
Output (deterministic): simple-icons.json.gz
a8f3b504cc21207e8d55d50f3724630a470968da69451c731538589c4debc87b

Fonts (@fontsource-variable 5.3.0, latin, SIL OFL 1.1), sha256:
4c8b93f431d462c696e12b9d6a033feb3394d36e66e781357c496b95d8a75e05  schibsted-grotesk-latin-wght-normal.woff2
18be452724bfdc236c074ca94a249a7f41a86752c7d04ab258ce9ed5651f6a7e  jetbrains-mono-latin-wght-normal.woff2
15222412a95d70042e66e79bfb3b985653e0693e4d41703bc131bee9c5aa1aa7  OFL-schibsted-grotesk.txt
403581b69dac5cff4079205e01c6b467e56af449ecbd7247693ddb1baafa005b  OFL-jetbrains-mono.txt
from https://cdn.jsdelivr.net/npm/@fontsource-variable/{schibsted-grotesk,jetbrains-mono}@5.3.0/

Claude-Session: https://claude.ai/code/session_01GAZsp3P7Qto6JnhipxWUJQ
This commit is contained in:
2026-10-13 10:06:09 +02:00
parent 9d5f24b125
commit 11c876af7f
9 changed files with 504 additions and 0 deletions
+113
View File
@@ -0,0 +1,113 @@
// Package icons maps container image references to brand icons from the
// Simple Icons set (CC0, see LICENSE-simple-icons.md). Only the master's UI
// imports it, so the agent binary does not carry the data.
package icons
import (
"bytes"
"compress/gzip"
_ "embed"
"encoding/json"
"strings"
"sync"
"github.com/shcizo/package-updater/internal/imageref"
)
//go:generate go run ./gen
//go:embed simple-icons.json.gz
var data []byte
var (
once sync.Once
paths map[string]string
)
// aliases maps a normalised image name to the Simple Icons slug that differs
// from it.
var aliases = map[string]string{
"postgres": "postgresql",
"goauthentik": "authentik",
"traefik": "traefikproxy",
"node": "nodedotjs",
"mongo": "mongodb",
}
// skipNamespaces are registry namespaces that name a publisher, not a product.
var skipNamespaces = map[string]bool{"library": true, "linuxserver": true, "bitnami": true, "hotio": true}
// suffixes are stripped from the last path segment to find the product name
// ("authentik-server" is authentik).
var suffixes = []string{"-server", "-web", "-api", "-worker", "-agent"}
func load() {
once.Do(func() {
paths = map[string]string{}
zr, err := gzip.NewReader(bytes.NewReader(data))
if err != nil {
return
}
_ = json.NewDecoder(zr).Decode(&paths)
})
}
// Path returns the SVG path data (24x24 viewBox) of the icon with this Simple
// Icons slug.
func Path(slug string) (string, bool) {
load()
p, ok := paths[slug]
return p, ok
}
// Count returns how many icons are embedded.
func Count() int {
load()
return len(paths)
}
// ForImage returns the slug of the icon that best matches the image
// reference, or "" when there is none. It tries the last path segment, that
// segment without a role suffix, and the namespace, in that order.
func ForImage(ref string) string {
segs := strings.Split(imageref.Parse(ref).Repo, "/")
if len(segs) > 1 && isRegistryHost(segs[0]) {
segs = segs[1:]
}
last := segs[len(segs)-1]
candidates := []string{last}
for _, s := range suffixes {
if base, ok := strings.CutSuffix(last, s); ok && base != "" {
candidates = append(candidates, base)
break
}
}
if len(segs) > 1 && !skipNamespaces[segs[len(segs)-2]] {
candidates = append(candidates, segs[len(segs)-2])
}
for _, c := range candidates {
slug := normalise(c)
if a, ok := aliases[slug]; ok {
slug = a
}
if _, ok := Path(slug); ok {
return slug
}
}
return ""
}
func isRegistryHost(s string) bool {
return strings.ContainsAny(s, ".:") || s == "localhost"
}
// normalise lowercases s and keeps only a-z and 0-9, the form of Simple Icons slugs.
func normalise(s string) string {
var b strings.Builder
for _, r := range strings.ToLower(s) {
if r >= 'a' && r <= 'z' || r >= '0' && r <= '9' {
b.WriteRune(r)
}
}
return b.String()
}