feat: live-ticking In Session timer using active session start times

SidebarStatsBar now computes active session elapsed time client-side
from started_at timestamps, ticking every 60s. Backend only returns
completed session minutes to avoid double-counting.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
chihlasm
2026-03-15 15:50:59 -04:00
parent 5faa654b7b
commit d0af8357a6
3 changed files with 35 additions and 7 deletions

View File

@@ -65,16 +65,17 @@ async def get_sidebar_stats(
)
active_count = active_result.scalar() or 0
# --- Total session minutes today ---
# --- Completed session minutes today (active session time computed client-side) ---
duration_expr = func.extract(
"epoch",
func.coalesce(Session.completed_at, now_utc) - Session.started_at,
Session.completed_at - Session.started_at,
) / 60.0
duration_result = await db.execute(
select(func.coalesce(func.sum(duration_expr), 0)).where(
and_(
user_filter,
Session.started_at.isnot(None),
Session.completed_at.isnot(None),
Session.started_at >= today_start_utc,
)
)

View File

@@ -104,7 +104,8 @@ export function Sidebar() {
<SidebarStatsBar
resolved={stats?.resolved_today ?? 0}
active={stats?.active_count ?? 0}
sessionMinutes={stats?.total_session_minutes_today ?? 0}
completedMinutes={stats?.total_session_minutes_today ?? 0}
activeSessionStartTimes={stats?.active_sessions.map(s => s.started_at) ?? []}
/>
{/* Activity Feed */}

View File

@@ -1,7 +1,12 @@
import { useEffect, useState } from 'react'
interface SidebarStatsBarProps {
resolved: number
active: number
sessionMinutes: number
/** Minutes from completed sessions today (server-computed) */
completedMinutes: number
/** Start times of currently active sessions (ISO strings) */
activeSessionStartTimes: string[]
}
function formatDuration(minutes: number): string {
@@ -11,7 +16,28 @@ function formatDuration(minutes: number): string {
return m > 0 ? `${h}h ${m}m` : `${h}h`
}
export function SidebarStatsBar({ resolved, active, sessionMinutes }: SidebarStatsBarProps) {
function calcActiveMinutes(startTimes: string[]): number {
const now = Date.now()
return startTimes.reduce((sum, st) => {
const elapsed = Math.floor((now - new Date(st).getTime()) / 60000)
return sum + Math.max(0, elapsed)
}, 0)
}
export function SidebarStatsBar({ resolved, active, completedMinutes, activeSessionStartTimes }: SidebarStatsBarProps) {
const [liveMinutes, setLiveMinutes] = useState(() => calcActiveMinutes(activeSessionStartTimes))
// Tick every 60s to keep the timer live
useEffect(() => {
setLiveMinutes(calcActiveMinutes(activeSessionStartTimes))
const interval = setInterval(() => {
setLiveMinutes(calcActiveMinutes(activeSessionStartTimes))
}, 60000)
return () => clearInterval(interval)
}, [activeSessionStartTimes])
const totalMinutes = completedMinutes + liveMinutes
return (
<div
className="flex gap-0.5 px-3 pt-2 pb-1"
@@ -45,9 +71,9 @@ export function SidebarStatsBar({ resolved, active, sessionMinutes }: SidebarSta
<div className="flex-1 rounded-md bg-[rgba(255,255,255,0.02)] px-1 py-1.5 text-center">
<div
className="font-label text-sm font-semibold leading-none text-muted-foreground"
aria-label={`${formatDuration(sessionMinutes)} in session today`}
aria-label={`${formatDuration(totalMinutes)} in session today`}
>
{formatDuration(sessionMinutes)}
{formatDuration(totalMinutes)}
</div>
<div className="mt-1 font-label text-[7px] uppercase tracking-[0.1em] text-[#3d4350]">
In Session