Add refresh button to Docker widget with server cache bust
ci/woodpecker/push/woodpecker Pipeline was successful

This commit is contained in:
2026-05-16 18:11:49 +02:00
parent fc0ad6e68e
commit be16444e93
2 changed files with 37 additions and 12 deletions
+5 -1
View File
@@ -123,15 +123,19 @@ async function getLscrLatestDigest(image: string, tag: string): Promise<string |
return getGenericRegistryDigest('lscr.io', image, tag, process.env.GITHUB_TOKEN) return getGenericRegistryDigest('lscr.io', image, tag, process.env.GITHUB_TOKEN)
} }
router.get('/docker', async (_req, res) => { router.get('/docker', async (req, res) => {
const host = process.env.PORTAINER_HOST const host = process.env.PORTAINER_HOST
if (!host) { if (!host) {
res.status(503).json({ error: 'PORTAINER_HOST not configured' }) res.status(503).json({ error: 'PORTAINER_HOST not configured' })
return return
} }
if (!req.query.refresh) {
const cached = fromCache<{ containers: ContainerInfo[] }>('docker:full', HUB_TTL) const cached = fromCache<{ containers: ContainerInfo[] }>('docker:full', HUB_TTL)
if (cached) { res.json(cached); return } if (cached) { res.json(cached); return }
} else {
delete cache['docker:full']
}
try { try {
const headers = portainerHeaders() const headers = portainerHeaders()
+25 -4
View File
@@ -15,14 +15,20 @@ 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)
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [refreshing, setRefreshing] = useState(false)
useEffect(() => { const load = (bust = false) => {
fetch('/api/updates/docker') if (bust) setRefreshing(true)
else setLoading(true)
setError(null)
fetch(`/api/updates/docker${bust ? '?refresh=1' : ''}`)
.then(r => r.json()) .then(r => r.json())
.then(d => { if (d.error) setError(d.error); else setContainers(d.containers) }) .then(d => { 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); setRefreshing(false) })
}, []) }
useEffect(() => { load() }, [])
const outdated = containers.filter(c => c.upToDate === false) const outdated = containers.filter(c => c.upToDate === false)
const upToDate = containers.filter(c => c.upToDate === true) const upToDate = containers.filter(c => c.upToDate === true)
@@ -36,11 +42,26 @@ export function DockerUpdatesWidget() {
<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>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
{!loading && !error && ( {!loading && !error && (
<span className="widget-badge" style={outdated.length > 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.length > 0 ? `${outdated.length} outdated` : 'all current'} {outdated.length > 0 ? `${outdated.length} outdated` : 'all current'}
</span> </span>
)} )}
<button
onClick={() => load(true)}
disabled={refreshing || loading}
title="Re-check all images"
style={{
background: 'none', border: '1px solid var(--border)', borderRadius: 6,
color: 'var(--muted)', cursor: 'pointer', padding: '2px 6px', fontSize: 11,
lineHeight: 1, transition: 'color 0.15s',
opacity: refreshing || loading ? 0.4 : 1,
}}
>
{refreshing ? '…' : '↺'}
</button>
</div>
</div> </div>
{loading && <div className="widget-loading">Checking images</div>} {loading && <div className="widget-loading">Checking images</div>}