Gitea Test
First Push to giTea
This commit is contained in:
@@ -14,6 +14,9 @@ import { QbittorrentWidget } from './components/widgets/QbittorrentWidget'
|
||||
import { GrafanaWidget } from './components/widgets/GrafanaWidget'
|
||||
import { PrometheusWidget } from './components/widgets/PrometheusWidget'
|
||||
import { LokiWidget } from './components/widgets/LokiWidget'
|
||||
import { JellyfinWidget } from './components/widgets/JellyfinWidget'
|
||||
import { NavidromeWidget } from './components/widgets/NavidromeWidget'
|
||||
import { RommWidget } from './components/widgets/RommWidget'
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
@@ -34,6 +37,9 @@ export default function App() {
|
||||
|
||||
<div className="section-label">Media</div>
|
||||
<div className="infra-grid">
|
||||
<JellyfinWidget />
|
||||
<NavidromeWidget />
|
||||
<RommWidget />
|
||||
<ArrCalendarWidget />
|
||||
<ArrStatsWidget />
|
||||
<QbittorrentWidget />
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import { useApi } from '../../hooks/useApi'
|
||||
import { JellyfinData } from '../../types'
|
||||
|
||||
export function JellyfinWidget() {
|
||||
const { data, loading, error } = useApi<JellyfinData>('/api/jellyfin/status', 30_000)
|
||||
|
||||
return (
|
||||
<div className="card">
|
||||
<div className="widget-header">
|
||||
<div className="widget-title">
|
||||
<span className="dot" style={{ background: '#00a4dc' }} />
|
||||
Jellyfin
|
||||
</div>
|
||||
{data && (
|
||||
<div className="widget-badge">
|
||||
{data.sessions.length > 0 ? `${data.sessions.length} playing` : 'idle'}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{loading && <div className="widget-loading">Connecting…</div>}
|
||||
{error && <div className="widget-error">⚠ {error}</div>}
|
||||
|
||||
{data && (
|
||||
<>
|
||||
<div className="stat-row" style={{ marginBottom: 14 }}>
|
||||
<div className="stat-item">
|
||||
<div className="stat-value accent">{data.library.movies.toLocaleString()}</div>
|
||||
<div className="stat-label">Movies</div>
|
||||
</div>
|
||||
<div className="stat-item">
|
||||
<div className="stat-value">{data.library.episodes.toLocaleString()}</div>
|
||||
<div className="stat-label">Episodes</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{data.sessions.length === 0 ? (
|
||||
<div style={{ fontSize: 12, color: 'var(--muted)' }}>No active streams</div>
|
||||
) : (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8, maxHeight: 200, overflowY: 'auto' }}>
|
||||
{data.sessions.map((s, i) => (
|
||||
<div key={i} style={{
|
||||
background: 'var(--surface2)',
|
||||
borderRadius: 6,
|
||||
padding: '8px 10px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 4,
|
||||
}}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<span style={{ fontSize: 12, fontWeight: 600, color: 'var(--text)' }}>{s.title}</span>
|
||||
<span style={{ fontSize: 10, color: s.paused ? 'var(--yellow)' : 'var(--green)' }}>
|
||||
{s.paused ? '⏸ paused' : '▶ playing'}
|
||||
</span>
|
||||
</div>
|
||||
{s.subtitle && (
|
||||
<div style={{ fontSize: 11, color: 'var(--text2)' }}>{s.subtitle}</div>
|
||||
)}
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<span style={{ fontSize: 11, color: 'var(--muted)' }}>{s.user} · {s.client}</span>
|
||||
{s.progress !== null && (
|
||||
<span style={{ fontSize: 11, color: 'var(--muted)' }}>{s.progress}%</span>
|
||||
)}
|
||||
</div>
|
||||
{s.progress !== null && (
|
||||
<div className="progress-track" style={{ marginTop: 2 }}>
|
||||
<div className="progress-fill green" style={{ width: `${s.progress}%` }} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import { useApi } from '../../hooks/useApi'
|
||||
import { NavidromeData } from '../../types'
|
||||
|
||||
export function NavidromeWidget() {
|
||||
const { data, loading, error } = useApi<NavidromeData>('/api/navidrome/status', 300_000)
|
||||
|
||||
return (
|
||||
<div className="card">
|
||||
<div className="widget-header">
|
||||
<div className="widget-title">
|
||||
<span className="dot" style={{ background: '#ff6600' }} />
|
||||
Navidrome
|
||||
</div>
|
||||
{data && (
|
||||
<div className="widget-badge">
|
||||
{data.nowPlaying.length > 0 ? `${data.nowPlaying.length} listening` : 'idle'}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{loading && <div className="widget-loading">Connecting…</div>}
|
||||
{error && <div className="widget-error">⚠ {error}</div>}
|
||||
|
||||
{data && (
|
||||
<>
|
||||
<div className="stat-row" style={{ marginBottom: 14 }}>
|
||||
<div className="stat-item">
|
||||
<div className="stat-value accent">{data.artistCount.toLocaleString()}</div>
|
||||
<div className="stat-label">Artists</div>
|
||||
</div>
|
||||
<div className="stat-item">
|
||||
<div className="stat-value">{data.albumCount.toLocaleString()}</div>
|
||||
<div className="stat-label">Albums</div>
|
||||
</div>
|
||||
<div className="stat-item">
|
||||
<div className="stat-value">{data.songCount.toLocaleString()}</div>
|
||||
<div className="stat-label">Songs</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{data.nowPlaying.length === 0 ? (
|
||||
<div style={{ fontSize: 12, color: 'var(--muted)' }}>Nothing playing</div>
|
||||
) : (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8, maxHeight: 200, overflowY: 'auto' }}>
|
||||
{data.nowPlaying.map((n, i) => (
|
||||
<div key={i} style={{
|
||||
background: 'var(--surface2)',
|
||||
borderRadius: 6,
|
||||
padding: '8px 10px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 3,
|
||||
}}>
|
||||
<div style={{ fontSize: 12, fontWeight: 600, color: 'var(--text)' }}>{n.title}</div>
|
||||
<div style={{ fontSize: 11, color: 'var(--muted)' }}>{n.artist} · {n.album}</div>
|
||||
<div style={{ fontSize: 10, color: 'var(--text2)' }}>▶ {n.user}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import { useApi } from '../../hooks/useApi'
|
||||
import { RommData } from '../../types'
|
||||
|
||||
export function RommWidget() {
|
||||
const { data, loading, error } = useApi<RommData>('/api/romm/status', 120_000)
|
||||
|
||||
return (
|
||||
<div className="card">
|
||||
<div className="widget-header">
|
||||
<div className="widget-title">
|
||||
<span className="dot" style={{ background: '#a855f7' }} />
|
||||
RomM
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{loading && <div className="widget-loading">Connecting…</div>}
|
||||
{error && <div className="widget-error">⚠ {error}</div>}
|
||||
|
||||
{data && (
|
||||
<>
|
||||
<div className="stat-row" style={{ marginBottom: 14 }}>
|
||||
<div className="stat-item">
|
||||
<div className="stat-value accent">{data.romCount.toLocaleString()}</div>
|
||||
<div className="stat-label">ROMs</div>
|
||||
</div>
|
||||
<div className="stat-item">
|
||||
<div className="stat-value">{data.platformCount}</div>
|
||||
<div className="stat-label">Platforms</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{data.platforms.length > 0 && (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 4, maxHeight: 160, overflowY: 'auto' }}>
|
||||
{data.platforms.map((p, i) => (
|
||||
<div key={i} style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
padding: '4px 0',
|
||||
borderBottom: i < data.platforms.length - 1 ? '1px solid var(--border)' : 'none',
|
||||
}}>
|
||||
<span style={{ fontSize: 12, color: 'var(--text)' }}>{p.name}</span>
|
||||
{p.romCount !== null && (
|
||||
<span style={{ fontSize: 11, color: 'var(--muted)', fontFamily: "'JetBrains Mono', monospace" }}>
|
||||
{p.romCount}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -142,3 +142,40 @@ export interface HeadscaleData {
|
||||
online: number
|
||||
nodes: HeadscaleNode[]
|
||||
}
|
||||
|
||||
export interface JellyfinSession {
|
||||
user: string
|
||||
title: string
|
||||
subtitle: string | null
|
||||
type: string
|
||||
progress: number | null
|
||||
paused: boolean
|
||||
client: string
|
||||
}
|
||||
export interface JellyfinData {
|
||||
sessions: JellyfinSession[]
|
||||
library: { movies: number; episodes: number; songs: number; albums: number }
|
||||
}
|
||||
|
||||
export interface NavidromeNowPlaying {
|
||||
user: string
|
||||
title: string
|
||||
artist: string
|
||||
album: string
|
||||
}
|
||||
export interface NavidromeData {
|
||||
artistCount: number
|
||||
albumCount: number
|
||||
songCount: number
|
||||
nowPlaying: NavidromeNowPlaying[]
|
||||
}
|
||||
|
||||
export interface RommPlatform {
|
||||
name: string
|
||||
romCount: number | null
|
||||
}
|
||||
export interface RommData {
|
||||
platformCount: number
|
||||
romCount: number
|
||||
platforms: RommPlatform[]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user