Fix lscr.io image update verification; tighten Docker widget
lscr.io is backed by ghcr.io and requires a GitHub token for registry auth — passing it fixes the silent null return from the token endpoint. Widget now shows only confirmed outdated/current containers, with unverified and unsupported-registry images summarised as a footer count.
This commit is contained in:
@@ -119,7 +119,8 @@ async function getGhcrLatestDigest(image: string, tag: string): Promise<string |
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function getLscrLatestDigest(image: string, tag: string): Promise<string | null> {
|
async function getLscrLatestDigest(image: string, tag: string): Promise<string | null> {
|
||||||
return getGenericRegistryDigest('lscr.io', image, tag)
|
// lscr.io is backed by ghcr.io and requires the same GitHub token auth
|
||||||
|
return getGenericRegistryDigest('lscr.io', image, tag, process.env.GITHUB_TOKEN)
|
||||||
}
|
}
|
||||||
|
|
||||||
router.get('/docker', async (_req, res) => {
|
router.get('/docker', async (_req, res) => {
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ interface ContainerInfo {
|
|||||||
endpoint: string
|
endpoint: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const KNOWN = ['docker.io', 'ghcr.io', 'lscr.io']
|
||||||
|
|
||||||
export function DockerUpdatesWidget() {
|
export function DockerUpdatesWidget() {
|
||||||
const [containers, setContainers] = useState<ContainerInfo[]>([])
|
const [containers, setContainers] = useState<ContainerInfo[]>([])
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
@@ -17,25 +19,26 @@ export function DockerUpdatesWidget() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetch('/api/updates/docker')
|
fetch('/api/updates/docker')
|
||||||
.then(r => r.json())
|
.then(r => r.json())
|
||||||
.then(d => {
|
.then(d => { if (d.error) setError(d.error); else setContainers(d.containers) })
|
||||||
if (d.error) setError(d.error)
|
|
||||||
else setContainers(d.containers)
|
|
||||||
})
|
|
||||||
.catch(() => setError('Failed to connect'))
|
.catch(() => setError('Failed to connect'))
|
||||||
.finally(() => setLoading(false))
|
.finally(() => setLoading(false))
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const outdated = containers.filter(c => c.upToDate === false).length
|
const outdated = containers.filter(c => c.upToDate === false)
|
||||||
const knownRegistries = ['docker.io', 'ghcr.io', 'lscr.io']
|
const upToDate = containers.filter(c => c.upToDate === true)
|
||||||
const unknown = containers.filter(c => c.upToDate === null && !knownRegistries.includes(c.registry)).length
|
const unverified = containers.filter(c => c.upToDate === null && KNOWN.includes(c.registry))
|
||||||
|
const external = containers.filter(c => !KNOWN.includes(c.registry))
|
||||||
|
|
||||||
|
// Show: outdated first, then up to date. Unverified and ext only as a footer count.
|
||||||
|
const visible = [...outdated, ...upToDate]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="card">
|
<div className="card">
|
||||||
<div className="widget-header">
|
<div className="widget-header">
|
||||||
<div className="widget-title"><span className="dot" />Docker</div>
|
<div className="widget-title"><span className="dot" />Docker</div>
|
||||||
{!loading && !error && (
|
{!loading && !error && (
|
||||||
<span className="widget-badge" style={outdated > 0 ? { background: 'rgba(248,113,113,0.15)', color: 'var(--red)' } : {}}>
|
<span className="widget-badge" style={outdated.length > 0 ? { background: 'rgba(248,113,113,0.15)', color: 'var(--red)' } : {}}>
|
||||||
{outdated > 0 ? `${outdated} outdated` : 'all current'}
|
{outdated.length > 0 ? `${outdated.length} outdated` : 'all current'}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -45,13 +48,7 @@ export function DockerUpdatesWidget() {
|
|||||||
|
|
||||||
{!loading && !error && (
|
{!loading && !error && (
|
||||||
<div className="progress-group" style={{ maxHeight: '260px', overflowY: 'auto', scrollbarWidth: 'thin', paddingRight: '4px' }}>
|
<div className="progress-group" style={{ maxHeight: '260px', overflowY: 'auto', scrollbarWidth: 'thin', paddingRight: '4px' }}>
|
||||||
{[...containers]
|
{visible.map(c => (
|
||||||
.sort((a, b) => {
|
|
||||||
const rank = (c: ContainerInfo) =>
|
|
||||||
c.upToDate === false ? 0 : c.upToDate === null ? 1 : 2
|
|
||||||
return rank(a) - rank(b)
|
|
||||||
})
|
|
||||||
.map(c => (
|
|
||||||
<div key={c.name} className="list-item">
|
<div key={c.name} className="list-item">
|
||||||
<div className="list-item-left" style={{ flexDirection: 'column', alignItems: 'flex-start', gap: 2 }}>
|
<div className="list-item-left" style={{ flexDirection: 'column', alignItems: 'flex-start', gap: 2 }}>
|
||||||
<span>{c.name}</span>
|
<span>{c.name}</span>
|
||||||
@@ -59,20 +56,17 @@ export function DockerUpdatesWidget() {
|
|||||||
{c.image}:{c.tag.startsWith('sha256:') ? c.tag.slice(0, 15) + '…' : c.tag} · {c.endpoint}
|
{c.image}:{c.tag.startsWith('sha256:') ? c.tag.slice(0, 15) + '…' : c.tag} · {c.endpoint}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{c.upToDate === null && !knownRegistries.includes(c.registry) ? (
|
{c.upToDate === true
|
||||||
<span className="pill pill-blue">ext</span>
|
? <span className="pill pill-green">✓</span>
|
||||||
) : c.upToDate === true ? (
|
: <span className="pill pill-red">↑ update</span>
|
||||||
<span className="pill pill-green">✓</span>
|
}
|
||||||
) : c.upToDate === false ? (
|
|
||||||
<span className="pill pill-red">↑ update</span>
|
|
||||||
) : (
|
|
||||||
<span className="pill" style={{ background: 'var(--surface2)', color: 'var(--muted)' }}>?</span>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{unknown > 0 && (
|
|
||||||
<div style={{ fontFamily: 'JetBrains Mono, monospace', fontSize: 10, color: 'var(--muted)', marginTop: 6 }}>
|
{(unverified.length > 0 || external.length > 0) && (
|
||||||
{unknown} non-Docker Hub image{unknown > 1 ? 's' : ''} not checked
|
<div style={{ fontFamily: 'JetBrains Mono, monospace', fontSize: 10, color: 'var(--muted)', marginTop: 8, lineHeight: 1.6 }}>
|
||||||
|
{unverified.length > 0 && <div>{unverified.length} image{unverified.length > 1 ? 's' : ''} could not be verified</div>}
|
||||||
|
{external.length > 0 && <div>{external.length} image{external.length > 1 ? 's' : ''} on unsupported registry</div>}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user