Compare commits
8 Commits
92cb6a86e2
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| ab42494d97 | |||
| 5ac98d966c | |||
| d2a4f3dbad | |||
| dca9312753 | |||
| dfcce073db | |||
| be16444e93 | |||
| fc0ad6e68e | |||
| 638c7d524f |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 512 512"><linearGradient id="audiobookshelf_svg__a" x1="255.5" x2="255.5" y1="991.4" y2="496.6" gradientTransform="matrix(1 0 0 -1 0 1000)" gradientUnits="userSpaceOnUse"><stop offset=".32" style="stop-color:#cd9d49"/><stop offset=".99" style="stop-color:#875d27"/></linearGradient><circle cx="255.5" cy="256" r="247.4" style="fill:url(#audiobookshelf_svg__a)"/><path d="M256 0C114.6 0 0 114.6 0 256s114.6 256 256 256 256-114.6 256-256S397.4 0 256 0m-.5 503.4C118.9 503.4 8.1 392.6 8.1 256S118.9 8.6 255.5 8.6 502.9 119.4 502.9 256 392.1 503.4 255.5 503.4m160.7-265.8c-2-1.7-5.1-4.1-9.4-7v-32.8c0-83.6-67.7-151.3-151.3-151.3s-151.3 67.7-151.3 151.3v32.8c-4.2 2.9-7.3 5.4-9.4 7-1.7 1.4-2.7 3.5-2.7 5.8v39.3c0 2.2 1 4.3 2.7 5.8 4.7 3.9 15.4 12 32.1 20.4v3.8c0 10.3 6.6 18.6 14.8 18.6s14.8-8.4 14.8-18.6v-94.2c0-10.3-6.6-18.6-14.8-18.6-7.9 0-14.3 7.7-14.8 17.3v-19.4c0-71 57.6-128.6 128.6-128.6 71.1 0 128.6 57.6 128.6 128.6v19.4c-.5-9.7-7-17.3-14.8-17.3-8.2 0-14.8 8.4-14.8 18.6v94.2c0 10.3 6.6 18.6 14.8 18.6s14.8-8.4 14.8-18.6v-3.8c16.7-8.4 27.4-16.5 32.1-20.4 1.7-1.4 2.7-3.6 2.7-5.8v-39.3c-.1-2.3-1-4.4-2.7-5.8M202.7 401.3c9.9 0 17.9-8 17.9-17.9V182.8c0-9.9-8-17.9-17.9-17.9h-18.5c-9.9 0-17.9 8-17.9 17.9v200.6c0 9.9 8 17.9 17.9 17.9zM173.1 213h40.8v4.3h-40.8zm91.6 188.3c9.9 0 17.9-8 17.9-17.9V182.8c0-9.9-8-17.9-17.9-17.9h-18.5c-9.9 0-17.9 8-17.9 17.9v200.6c0 9.9 8 17.9 17.9 17.9zM235.1 213h40.8v4.3h-40.8zm91.7 188.3c9.9 0 17.9-8 17.9-17.9V182.8c0-9.9-8-17.9-17.9-17.9h-18.5c-9.9 0-17.9 8-17.9 17.9v200.6c0 9.9 8 17.9 17.9 17.9zM297.1 213h40.8v4.3h-40.8zM135.4 407.5h240.2c7.4 0 13.5 6 13.5 13.5 0 7.4-6 13.5-13.5 13.5H135.4c-7.4 0-13.5-6-13.5-13.5s6-13.5 13.5-13.5" style="fill:#fff"/></svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" id="readarr_svg__Layer_1" x="0" y="0" version="1.1" viewBox="0 0 512 512"><style>.readarr_svg__st0{fill:#eee}</style><circle id="readarr_svg__svg_2_00000098218664590018320060000008938703571635696276_" cx="256" cy="256" r="255.6" class="readarr_svg__st0"/><path d="M256 512c-34.6 0-68.1-6.8-99.6-20.1C125.9 479 98.5 460.5 75 437s-42-50.9-54.9-81.4C6.8 324.1 0 290.6 0 256s6.8-68.1 20.1-99.6C33 125.9 51.5 98.5 75 75s50.9-42 81.4-54.9C187.9 6.8 221.4 0 256 0s68.1 6.8 99.6 20.1C386.1 33 413.5 51.5 437 75s42 50.9 54.9 81.4C505.2 188 512 221.5 512 256s-6.8 68.1-20.1 99.6C479 386.1 460.5 413.5 437 437s-50.9 42-81.4 54.9c-31.5 13.3-65 20.1-99.6 20.1M256 .8C115.3.8.8 115.3.8 256S115.3 511.2 256 511.2 511.2 396.7 511.2 256 396.7.8 256 .8M459.6 170c-11.1-26.3-27.1-49.9-47.3-70.2s-44-36.2-70.3-47.4C314.8 40.9 285.8 35 256 35s-58.8 5.8-86 17.4c-26.3 11.1-49.9 27.1-70.2 47.3s-36.2 44-47.4 70.3C40.9 197.2 35 226.2 35 256s5.8 58.8 17.4 86c11.1 26.3 27.1 49.9 47.3 70.2s43.9 36.2 70.2 47.3c27.2 11.5 56.2 17.4 86 17.4s58.8-5.8 86-17.4c26.3-11.1 49.9-27.1 70.2-47.3s36.2-43.9 47.3-70.2c11.5-27.2 17.4-56.2 17.4-86 .1-29.8-5.7-58.8-17.2-86" style="fill:#443c3c"/><path d="M425.9 172.1c.1-6.3.2-9.9.3-10-.3-26.7-8.8-24-15.3-24.8-2.4.1-4.7.3-6.9.4-34.8-43.4-88.1-71.2-148-71.2s-113.4 27.8-148.1 71.3c-2.6-.2-5.3-.4-8.1-.5-6.5.8-14.9-1.9-15.3 24.8.1.1.2 4.7.3 12.5C73 199.3 66.4 226.9 66.4 256c0 31.5 7.7 61.2 21.3 87.3.3 1.2.9 2.6 1.9 3.6 32.2 58.7 94.6 98.6 166.3 98.6 104.6 0 189.5-84.8 189.5-189.5.1-30.1-7-58.6-19.5-83.9M255.3 389.9c-1.8 0-3.6 0-5.4-.1-.2-42.5-1.6-199.7-1.9-200 .3.3-3.5-13.1-10.2-15.6-1.5-.8-18.6-14.9-60.3-25.6 22.5-16.3 49.5-25.1 77.8-25.1s55.3 8.8 77.8 25c-41.6 10.7-58.7 24.9-60.2 25.6l-2.8 2c-5.9 3.5-6.6 8.1-6.2 7.8-.3.3-2.6 161.7-3 205.8-1.9.1-3.7.2-5.6.2" class="readarr_svg__st0"/><path d="M450 256c0-32-7.7-62.2-21.5-88.8 0-1.9.1-3.3.1-4.1l.1-.1v-1c-.3-24.2-7.2-26.6-15.5-27.1-.7 0-1.4-.1-2-.2h-.4c-1.1.1-2.2.1-3.2.2C372 90.5 317.3 62 256 62c-61.5 0-116.4 28.7-151.9 73.4-1.3-.1-2.7-.2-4-.2h-.4c-.6.1-1.3.1-2 .2-8.3.5-15.2 2.9-15.5 27.1v1l.1.1c0 1.1.1 3.1.1 6C69.4 195.5 62 224.9 62 256c0 32.8 8.2 63.7 22.5 90.8.1 8.1.2 15.8.3 22.7 0 2.9 1.5 8.5 7.2 8.9.3.1.8.2 1.6.3 4 .7 8.9 1.7 14.5 2.9C143.8 423.5 196.8 450 256 450s112.2-26.5 147.8-68.3c5.1-1.1 9.6-2 13.3-2.6.8-.1 1.3-.2 1.6-.3 5.7-.4 7.2-6 7.2-8.9.1-6.3.2-13.3.3-20.6 15.1-27.7 23.8-59.5 23.8-93.3m-383.1 0c0-26.8 5.6-52.2 15.7-75.3v.5q-.45 4.2-.6 9.6v.5c.3 6.4 1.5 84 2.4 144-11.2-24.1-17.5-51-17.5-79.3m185.6 172.9v-9c0-3.6 0-8.2-.1-13.7.3-1.5.4-3.4.1-5.6v-9c0-5.6-.1-13.6-.1-23.9-.1-19.1-.3-44.7-.6-72.3-.2-27.5-.5-53.2-.7-72.3-.1-10.3-.2-18.3-.3-23.9 0-2.9-.1-5.2-.1-6.7 0-.8 0-1.4-.1-1.9v-.5c0-.1 0-.2-.1-.4 0-.2-.1-.4-.1-.5-.8-3.3-4.5-14.1-11.4-16.9-.2-.1-.5-.3-.8-.5-16.2-11.3-54.1-30.4-128.2-35.9C144.7 93.8 197.2 67 255.9 67c58.4 0 110.7 26.6 145.4 68.4-74.6 5.4-112.6 24.7-128.9 36.1-.3.2-.6.4-.8.5-6.9 2.8-10.6 13.6-11.4 16.9-.1.1-.1.3-.1.5s-.1.3-.1.4v.4c0 .5 0 1.1-.1 2 0 1.6-.1 3.8-.1 6.7-.1 5.6-.2 13.6-.3 23.9-.2 19.1-.5 44.9-.7 72.5s-.5 53.3-.6 72.4c-.1 10.3-.1 18.3-.1 23.9v9c-.3 2-.2 3.7 0 5.2 0 5.8-.1 10.7-.1 14.4v9c-1 6.9 1.6 9.8 4 10.9 1 .5 2.3.8 3.4.8.8 0 1.4-.1 1.8-.4 42-32.5 94.6-49.1 128.2-57-34.6 37.8-84.3 61.6-139.5 61.6S151 421.4 116.4 383.6c33.6 8 85.4 24.6 126.9 56.6.4.3 1 .4 1.8.4 1.1 0 2.3-.3 3.4-.8 2.4-1.2 5-4 4-10.9m176.2-237.7v-.6c0-3.4-.2-6.4-.5-9 0-1.2 0-2.3.1-3.4 10.8 23.8 16.8 50.1 16.8 77.9 0 29.4-6.7 57.3-18.8 82.1.9-60.6 2.1-140.6 2.4-147m-17.9-51.5c.7.1 1.4.1 2.1.2 5.4.3 10.5.7 10.8 22.1 0 .5-.1 1.1-.1 2 0 1-.1 2.4-.1 4.1-2.8-3-6.2-3.2-9.2-3.4-.7 0-1.4-.1-2.1-.2h-.2c-81.7 4.4-122.7 24.7-139.8 36.4-.4.3-.8.5-.9.6-2.2 1.9-4.4 4.7-6.3 7.5.1-11.3.3-18.6.3-19.5.7-2.6 4.1-11.5 8.5-13.1l.2-.1c.3-.2.7-.4 1.4-.9 16.5-11.4 56.1-31.3 135.4-35.7m-312.8.6c.7 0 1.4-.1 2.1-.2 79.4 4.4 118.9 24.2 135.4 35.7.7.5 1.1.7 1.4.9l.2.1c4.6 1.7 8.1 11.4 8.5 13.4l.1 1.3c.1 2.5.1 8.1.2 16.1-1.7-2.6-3.9-4.9-6.4-5.9-.2-.1-.5-.4-.9-.6-17.1-11.7-58.1-31.9-139.8-36.3h-.2c-.7.1-1.4.1-2.1.2-3 .2-6.4.4-9.2 3.3 0-1.6-.1-2.9-.1-3.8s0-1.6-.1-2c.3-21.5 5.4-21.8 10.9-22.2" style="fill:#8e2222"/></svg>
|
||||
|
After Width: | Height: | Size: 4.1 KiB |
@@ -17,11 +17,15 @@ async function getCookie(): Promise<string> {
|
||||
const res = await axios.post(
|
||||
`${host}/api/v2/auth/login`,
|
||||
new URLSearchParams({ username: user, password: pass }),
|
||||
{ headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Referer': host, 'Origin': host } }
|
||||
{
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Referer': host, 'Origin': host },
|
||||
validateStatus: s => s < 400,
|
||||
}
|
||||
)
|
||||
|
||||
const cookie = (res.headers['set-cookie'] ?? []).find((c: string) => c.startsWith('SID='))
|
||||
if (!cookie || res.data === 'Fails.') throw new Error('qBittorrent login failed')
|
||||
// qBittorrent 5.x renamed the cookie from SID to QBT_SID_{port}
|
||||
const cookie = (res.headers['set-cookie'] ?? []).find((c: string) => /^(QBT_)?SID[_=]/.test(c))
|
||||
if (!cookie) throw new Error(`qBittorrent login failed (${res.status}): "${String(res.data).trim()}"`)
|
||||
|
||||
sid = cookie.split(';')[0]
|
||||
sidExpiry = Date.now() + 55 * 60 * 1000
|
||||
|
||||
@@ -123,15 +123,19 @@ async function getLscrLatestDigest(image: string, tag: string): Promise<string |
|
||||
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
|
||||
if (!host) {
|
||||
res.status(503).json({ error: 'PORTAINER_HOST not configured' })
|
||||
return
|
||||
}
|
||||
|
||||
if (!req.query.refresh) {
|
||||
const cached = fromCache<{ containers: ContainerInfo[] }>('docker:full', HUB_TTL)
|
||||
if (cached) { res.json(cached); return }
|
||||
} else {
|
||||
delete cache['docker:full']
|
||||
}
|
||||
|
||||
try {
|
||||
const headers = portainerHeaders()
|
||||
|
||||
@@ -52,12 +52,14 @@ export function Header() {
|
||||
<div className="logo-sub">homelab dashboard</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="topbar-right">
|
||||
|
||||
<nav className="nav-tabs">
|
||||
<NavLink to="/home" className={({ isActive }) => `nav-tab${isActive ? ' nav-tab-active' : ''}`}>Home</NavLink>
|
||||
<NavLink to="/dashboard" className={({ isActive }) => `nav-tab${isActive ? ' nav-tab-active' : ''}`}>Dashboard</NavLink>
|
||||
<NavLink to="/apps" className={({ isActive }) => `nav-tab${isActive ? ' nav-tab-active' : ''}`}>Apps</NavLink>
|
||||
</nav>
|
||||
|
||||
<div className="topbar-right">
|
||||
{weather && (
|
||||
<div className="weather-inline">
|
||||
<span className="weather-emoji">{weatherEmoji(weather.code)}</span>
|
||||
|
||||
@@ -15,14 +15,20 @@ export function DockerUpdatesWidget() {
|
||||
const [containers, setContainers] = useState<ContainerInfo[]>([])
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [refreshing, setRefreshing] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
fetch('/api/updates/docker')
|
||||
const load = (bust = false) => {
|
||||
if (bust) setRefreshing(true)
|
||||
else setLoading(true)
|
||||
setError(null)
|
||||
fetch(`/api/updates/docker${bust ? '?refresh=1' : ''}`)
|
||||
.then(r => r.json())
|
||||
.then(d => { if (d.error) setError(d.error); else setContainers(d.containers) })
|
||||
.catch(() => setError('Failed to connect'))
|
||||
.finally(() => setLoading(false))
|
||||
}, [])
|
||||
.finally(() => { setLoading(false); setRefreshing(false) })
|
||||
}
|
||||
|
||||
useEffect(() => { load() }, [])
|
||||
|
||||
const outdated = containers.filter(c => c.upToDate === false)
|
||||
const upToDate = containers.filter(c => c.upToDate === true)
|
||||
@@ -36,11 +42,26 @@ export function DockerUpdatesWidget() {
|
||||
<div className="card">
|
||||
<div className="widget-header">
|
||||
<div className="widget-title"><span className="dot" />Docker</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||
{!loading && !error && (
|
||||
<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'}
|
||||
</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>
|
||||
|
||||
{loading && <div className="widget-loading">Checking images…</div>}
|
||||
|
||||
@@ -36,10 +36,12 @@ export const appGroups: AppGroup[] = [
|
||||
name: 'Media',
|
||||
apps: [
|
||||
{ name: 'Jellyfin', url: 'https://jellyfin.syco.me', icon: `${ICONS}/jellyfin.svg` },
|
||||
{ name: 'Audiobookshelf', url: 'https://audiobooks.syco.me', icon: `${ICONS}/audiobookshelf.svg` },
|
||||
{ name: 'Navidrome', url: 'https://music.syco.me', icon: `${ICONS}/navidrome.svg` },
|
||||
{ name: 'Overseerr', url: 'https://overseerr.syco.me', icon: `${ICONS}/overseerr.svg` },
|
||||
{ name: 'Radarr', url: 'https://radarr.syco.me', icon: `${ICONS}/radarr.svg` },
|
||||
{ name: 'Sonarr', url: 'https://sonarr.syco.me', icon: `${ICONS}/sonarr.svg` },
|
||||
{ name: 'Readarr', url: 'https://readarr.syco.me', icon: `${ICONS}/readarr.svg` },
|
||||
{ name: 'Lidarr', url: 'https://lidarr.syco.me', icon: `${ICONS}/lidarr.svg` },
|
||||
{ name: 'Prowlarr', url: 'https://prowlarr.syco.me', icon: `${ICONS}/prowlarr.svg` },
|
||||
{ name: 'Bazarr', url: 'https://bazarr.syco.me', icon: `${ICONS}/bazarr.svg` },
|
||||
|
||||
+41
-3
@@ -53,12 +53,17 @@ body::before {
|
||||
|
||||
/* ── Header ── */
|
||||
header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 22px 0 18px;
|
||||
background: var(--bg);
|
||||
border-bottom: 1px solid var(--border);
|
||||
margin-bottom: 28px;
|
||||
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
.logo { display: flex; align-items: center; gap: 10px; }
|
||||
.logo-mark {
|
||||
@@ -244,11 +249,44 @@ header {
|
||||
}
|
||||
|
||||
/* ── Responsive ── */
|
||||
@media (max-width: 540px) {
|
||||
@media (max-width: 640px) {
|
||||
.shell { padding: 0 14px 32px; }
|
||||
.stat-value { font-size: 18px; }
|
||||
.clock-time { font-size: 16px; }
|
||||
.topbar-right { gap: 12px; }
|
||||
|
||||
/* Two-row header: [logo · clock] then [nav full-width] */
|
||||
header {
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 12px 0 10px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
/* Logo shrinks */
|
||||
.logo-sub { display: none; }
|
||||
.logo-text { font-size: 15px; }
|
||||
|
||||
/* Clock: time only, no date */
|
||||
.clock-time { font-size: 15px; letter-spacing: 0.5px; }
|
||||
.clock-date { display: none; }
|
||||
|
||||
/* Weather hidden — full card already on /home */
|
||||
.weather-inline { display: none !important; }
|
||||
|
||||
/* topbar-right (clock) pushed to far right on row 1 */
|
||||
.topbar-right { margin-left: auto; gap: 0; }
|
||||
|
||||
/* Nav drops to row 2, spans full width */
|
||||
.nav-tabs {
|
||||
order: 10;
|
||||
flex-basis: 100%;
|
||||
justify-content: stretch;
|
||||
}
|
||||
.nav-tab {
|
||||
flex: 1;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Nav tabs ── */
|
||||
|
||||
Reference in New Issue
Block a user