diff --git a/cmd/web-ui/static/modules/pages/dashboard.js b/cmd/web-ui/static/modules/pages/dashboard.js index 6912383..925c029 100644 --- a/cmd/web-ui/static/modules/pages/dashboard.js +++ b/cmd/web-ui/static/modules/pages/dashboard.js @@ -160,7 +160,14 @@ function renderSummaryCards() { const fws = Object.keys(s.by_framework || {}); if (fws.length > 0) { const sub = document.getElementById('dash-active-sub'); - if (sub) sub.textContent = fws.map(f => `${f} ${(s.by_framework[f].runs || 0)}`).join(' · '); + if (sub) { + const activeByFramework = fws + .map(f => [f, s.by_framework[f].active_sessions || 0]) + .filter(([, count]) => count > 0); + sub.textContent = activeByFramework.length > 0 + ? activeByFramework.map(([f, count]) => `${f} ${count}`).join(' · ') + : 'no live sessions'; + } } const errEl = document.getElementById('dash-errors'); diff --git a/internal/store/postgres/stats.go b/internal/store/postgres/stats.go index 78af03a..a8de0d9 100644 --- a/internal/store/postgres/stats.go +++ b/internal/store/postgres/stats.go @@ -6,9 +6,10 @@ import ( ) type FrameworkStats struct { - Runs int `json:"runs"` - Tools int `json:"tools"` - Errors int `json:"errors"` + ActiveSessions int `json:"active_sessions"` + Runs int `json:"runs"` + Tools int `json:"tools"` + Errors int `json:"errors"` } type Summary struct { @@ -44,23 +45,47 @@ func (d *DB) GetSummary(ctx context.Context) (*Summary, error) { now := time.Now() midnight := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()) - // Active sessions: sessions with a session.start but no session.end (last 7 days) + // Active sessions are open sessions with recent activity. Some hook sources can + // miss session.end, so "started but never ended" alone overstates live work. activeQ := ` - SELECT COUNT(DISTINCT e.session_id) - FROM events e - WHERE e.type = 'session.start' - AND e.session_id IS NOT NULL - AND e.ts >= $1 - AND NOT EXISTS ( - SELECT 1 - FROM events e2 - WHERE e2.type = 'session.end' - AND e2.session_id = e.session_id - ) + WITH session_groups AS ( + SELECT + session_id, + COALESCE(MAX(source_framework), 'unknown') AS framework, + MAX(ts) AS last_event_at, + BOOL_OR(type = 'session.start') AS has_start, + BOOL_OR(type = 'session.end') AS has_end + FROM events + WHERE session_id IS NOT NULL + GROUP BY session_id + ) + SELECT framework, COUNT(*) + FROM session_groups + WHERE has_start + AND NOT has_end + AND last_event_at >= $1 + GROUP BY framework ` + activeRows, err := d.sql.QueryContext(ctx, activeQ, now.Add(-15*time.Minute)) + if err != nil { + return nil, err + } + defer activeRows.Close() + + byFramework := make(map[string]FrameworkStats) var activeSessions int - activeSessionsSince := time.Now().Add(-7 * 24 * time.Hour) - if err := d.sql.QueryRowContext(ctx, activeQ, activeSessionsSince).Scan(&activeSessions); err != nil { + for activeRows.Next() { + var fw string + var count int + if err := activeRows.Scan(&fw, &count); err != nil { + return nil, err + } + fs := byFramework[fw] + fs.ActiveSessions = count + byFramework[fw] = fs + activeSessions += count + } + if err := activeRows.Err(); err != nil { return nil, err } @@ -83,7 +108,6 @@ func (d *DB) GetSummary(ctx context.Context) (*Summary, error) { } defer rows.Close() - byFramework := make(map[string]FrameworkStats) var totalRuns, totalTools, totalErrors int for rows.Next() { @@ -92,6 +116,9 @@ func (d *DB) GetSummary(ctx context.Context) (*Summary, error) { if err := rows.Scan(&fw, &fs.Runs, &fs.Tools, &fs.Errors); err != nil { return nil, err } + if existing, ok := byFramework[fw]; ok { + fs.ActiveSessions = existing.ActiveSessions + } byFramework[fw] = fs totalRuns += fs.Runs totalTools += fs.Tools