webui: add first tab data dashboard with KPI cards

This commit is contained in:
DBT
2026-02-25 13:43:42 +00:00
parent dd24023a91
commit 44cb2c61c2
2 changed files with 49 additions and 4 deletions

View File

@@ -4,7 +4,7 @@ type ChatItem = { role: 'user' | 'assistant'; text: string }
type Session = { key: string; title: string }
type CronJob = { id: string; name: string; enabled: boolean }
type Cfg = Record<string, any>
type View = 'chat' | 'config' | 'cron' | 'nodes'
type View = 'dashboard' | 'chat' | 'config' | 'cron' | 'nodes'
const defaultSessions: Session[] = [{ key: 'webui:default', title: 'Default' }]
@@ -25,7 +25,7 @@ function setPath(obj: any, path: string, value: any) {
}
export function App() {
const [view, setView] = useState<View>('chat')
const [view, setView] = useState<View>('dashboard')
const [token, setToken] = useState('')
const [cfg, setCfg] = useState<Cfg>({})
const [cfgRaw, setCfgRaw] = useState('{}')
@@ -38,6 +38,14 @@ export function App() {
const [cron, setCron] = useState<CronJob[]>([])
const activeChat = useMemo(() => chat[active] || [], [chat, active])
const q = token ? `?token=${encodeURIComponent(token)}` : ''
const onlineNodes = useMemo(() => {
try {
const arr = JSON.parse(nodes)
return Array.isArray(arr) ? arr.filter((n: any) => n?.online).length : 0
} catch {
return 0
}
}, [nodes])
async function loadConfig() {
const r = await fetch(`/webui/api/config${q}`)
@@ -69,6 +77,10 @@ export function App() {
await refreshCron()
}
async function refreshAll() {
await Promise.all([loadConfig(), refreshCron(), refreshNodes()])
}
async function send() {
let media = ''
const input = document.getElementById('file') as HTMLInputElement | null
@@ -110,6 +122,7 @@ export function App() {
<div className='shell'>
<nav className='left-menu'>
<button className={view==='dashboard'?'on':''} onClick={() => setView('dashboard')}>📊 Dashboard</button>
<button className={view==='chat'?'on':''} onClick={() => setView('chat')}>💬 Chat</button>
<button className={view==='config'?'on':''} onClick={() => setView('config')}> Config</button>
<button className={view==='cron'?'on':''} onClick={() => setView('cron')}> Cron</button>
@@ -124,6 +137,31 @@ export function App() {
</aside>
<main className='main'>
{view === 'dashboard' && (
<section className='panel'>
<div className='panel-title'>Data Dashboard</div>
<div className='row'><button onClick={refreshAll}>Refresh All</button></div>
<div className='kpi-grid'>
<div className='kpi'><div className='kpi-label'>Gateway</div><div className='kpi-value'>Online</div></div>
<div className='kpi'><div className='kpi-label'>Sessions</div><div className='kpi-value'>{sessions.length}</div></div>
<div className='kpi'><div className='kpi-label'>Cron Jobs</div><div className='kpi-value'>{cron.length}</div></div>
<div className='kpi'><div className='kpi-label'>Nodes Online</div><div className='kpi-value'>{onlineNodes}</div></div>
</div>
<div className='dashboard-panels'>
<div className='dashboard-card'>
<div className='panel-title'>Recent Cron</div>
<ul>
{cron.slice(0, 5).map((j) => <li key={j.id}>{j.name || j.id} {j.enabled ? '✅' : '⏸'}</li>)}
</ul>
</div>
<div className='dashboard-card'>
<div className='panel-title'>Nodes Snapshot</div>
<pre>{nodes}</pre>
</div>
</div>
</section>
)}
{view === 'chat' && (
<section className='panel'>
<div className='panel-title'>Chat</div>

View File

@@ -27,9 +27,16 @@ textarea{width:100%;min-height:240px;background:#0b1220;color:#e5e7eb;border:1px
.form-grid input{background:#0b1220;color:#fff;border:1px solid #374151;padding:8px;border-radius:8px}
.form-grid input[type='checkbox']{width:20px;height:20px;padding:0}
pre{flex:1;overflow:auto;background:#0b1220;border:1px solid #374151;border-radius:8px;padding:8px;white-space:pre-wrap}
.kpi-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px;margin-bottom:10px}
.kpi{background:#0b1220;border:1px solid #374151;border-radius:10px;padding:10px}
.kpi-label{font-size:12px;color:#9ca3af}
.kpi-value{font-size:22px;font-weight:800}
.dashboard-panels{display:grid;grid-template-columns:1fr 1fr;gap:10px;min-height:0;flex:1}
.dashboard-card{background:#0b1220;border:1px solid #374151;border-radius:10px;padding:8px;min-height:0;overflow:auto}
.dashboard-card ul{margin:0;padding-left:18px}
.cron-list{flex:1;overflow:auto;border:1px solid #374151;border-radius:8px;padding:6px;background:#0b1220}
.cron-item{display:flex;justify-content:space-between;gap:8px;align-items:center;border-bottom:1px solid #1f2937;padding:6px 0}
.cron-item:last-child{border-bottom:none}
.muted{font-size:12px;color:#9ca3af}
@media (max-width: 1024px){.shell{grid-template-columns:70px 170px 1fr}.topbar input{min-width:160px}}
@media (max-width: 768px){.shell{grid-template-columns:1fr;grid-template-rows:auto auto 1fr}.left-menu{flex-direction:row;overflow:auto}.sessions{max-height:140px}.form-grid{grid-template-columns:1fr}}
@media (max-width: 1024px){.shell{grid-template-columns:70px 170px 1fr}.topbar input{min-width:160px}.kpi-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.dashboard-panels{grid-template-columns:1fr}}
@media (max-width: 768px){.shell{grid-template-columns:1fr;grid-template-rows:auto auto 1fr}.left-menu{flex-direction:row;overflow:auto}.sessions{max-height:140px}.form-grid{grid-template-columns:1fr}.kpi-grid{grid-template-columns:1fr}}