Add Docker and GitHub release update tracker widgets
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/push/woodpecker Pipeline was successful
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
interface ContainerInfo {
|
||||
name: string
|
||||
image: string
|
||||
tag: string
|
||||
upToDate: boolean | null
|
||||
registry: string
|
||||
}
|
||||
|
||||
export function DockerUpdatesWidget() {
|
||||
const [containers, setContainers] = useState<ContainerInfo[]>([])
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
fetch('/api/updates/docker')
|
||||
.then(r => r.json())
|
||||
.then(d => {
|
||||
if (d.error) setError(d.error)
|
||||
else setContainers(d.containers)
|
||||
})
|
||||
.catch(() => setError('Failed to connect'))
|
||||
.finally(() => setLoading(false))
|
||||
}, [])
|
||||
|
||||
const outdated = containers.filter(c => c.upToDate === false).length
|
||||
const unknown = containers.filter(c => c.upToDate === null).length
|
||||
|
||||
return (
|
||||
<div className="card">
|
||||
<div className="widget-header">
|
||||
<div className="widget-title"><span className="dot" />Docker</div>
|
||||
{!loading && !error && (
|
||||
<span className="widget-badge" style={outdated > 0 ? { background: 'rgba(248,113,113,0.15)', color: 'var(--red)' } : {}}>
|
||||
{outdated > 0 ? `${outdated} outdated` : 'all current'}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{loading && <div className="widget-loading">Checking images…</div>}
|
||||
{error && <div className="widget-error">⚠ {error}</div>}
|
||||
|
||||
{!loading && !error && (
|
||||
<div className="progress-group">
|
||||
{containers.map(c => (
|
||||
<div key={c.name} className="list-item">
|
||||
<div className="list-item-left">
|
||||
<span>{c.name}</span>
|
||||
<span style={{ fontFamily: 'JetBrains Mono, monospace', fontSize: 10, color: 'var(--muted)' }}>
|
||||
{c.image}:{c.tag}
|
||||
</span>
|
||||
</div>
|
||||
{c.registry !== 'docker.io' ? (
|
||||
<span className="pill pill-blue">ext</span>
|
||||
) : c.upToDate === true ? (
|
||||
<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>
|
||||
))}
|
||||
{unknown > 0 && (
|
||||
<div style={{ fontFamily: 'JetBrains Mono, monospace', fontSize: 10, color: 'var(--muted)', marginTop: 6 }}>
|
||||
{unknown} non-Docker Hub image{unknown > 1 ? 's' : ''} not checked
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
interface Release {
|
||||
repo: string
|
||||
version: string
|
||||
name: string
|
||||
publishedAt: string
|
||||
url: string
|
||||
}
|
||||
|
||||
function timeAgo(iso: string): string {
|
||||
const diff = Date.now() - new Date(iso).getTime()
|
||||
const days = Math.floor(diff / 86400000)
|
||||
if (days === 0) return 'today'
|
||||
if (days === 1) return 'yesterday'
|
||||
if (days < 30) return `${days}d ago`
|
||||
const months = Math.floor(days / 30)
|
||||
if (months < 12) return `${months}mo ago`
|
||||
return `${Math.floor(months / 12)}y ago`
|
||||
}
|
||||
|
||||
export function GithubReleasesWidget() {
|
||||
const [releases, setReleases] = useState<Release[]>([])
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
fetch('/api/updates/github')
|
||||
.then(r => r.json())
|
||||
.then(d => {
|
||||
if (d.error) setError(d.error)
|
||||
else setReleases(d.releases)
|
||||
})
|
||||
.catch(() => setError('Failed to fetch releases'))
|
||||
.finally(() => setLoading(false))
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="card">
|
||||
<div className="widget-header">
|
||||
<div className="widget-title"><span className="dot" />GitHub Releases</div>
|
||||
{!loading && !error && (
|
||||
<span className="widget-badge">{releases.length} tracked</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{loading && <div className="widget-loading">Fetching releases…</div>}
|
||||
{error && <div className="widget-error">⚠ {error}</div>}
|
||||
|
||||
{!loading && !error && (
|
||||
<div className="progress-group">
|
||||
{releases.map(r => (
|
||||
<div key={r.repo} className="list-item">
|
||||
<div className="list-item-left" style={{ flexDirection: 'column', alignItems: 'flex-start', gap: 2 }}>
|
||||
<a
|
||||
href={r.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={{ color: 'var(--text)', textDecoration: 'none', fontSize: 12 }}
|
||||
>
|
||||
{r.repo.split('/')[1]}
|
||||
</a>
|
||||
<span style={{ fontFamily: 'JetBrains Mono, monospace', fontSize: 10, color: 'var(--muted)' }}>
|
||||
{r.repo.split('/')[0]}
|
||||
</span>
|
||||
</div>
|
||||
<div style={{ textAlign: 'right' }}>
|
||||
<div className="pill pill-green" style={{ marginBottom: 3 }}>{r.version}</div>
|
||||
<div style={{ fontFamily: 'JetBrains Mono, monospace', fontSize: 10, color: 'var(--muted)' }}>
|
||||
{timeAgo(r.publishedAt)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -17,6 +17,8 @@ import { PrometheusWidget } from '../components/widgets/PrometheusWidget'
|
||||
import { LokiWidget } from '../components/widgets/LokiWidget'
|
||||
import { AuthentikWidget } from '../components/widgets/AuthentikWidget'
|
||||
import { VaultwardenWidget } from '../components/widgets/VaultwardenWidget'
|
||||
import { DockerUpdatesWidget } from '../components/widgets/DockerUpdatesWidget'
|
||||
import { GithubReleasesWidget } from '../components/widgets/GithubReleasesWidget'
|
||||
|
||||
export interface DashboardSection {
|
||||
label: string
|
||||
@@ -40,4 +42,8 @@ export const dashboardSections: DashboardSection[] = [
|
||||
label: 'Access',
|
||||
widgets: [AuthentikWidget, VaultwardenWidget],
|
||||
},
|
||||
{
|
||||
label: 'Updates',
|
||||
widgets: [DockerUpdatesWidget, GithubReleasesWidget],
|
||||
},
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user