"""
FocusTrack - Page 1: Live Dashboard
Real-time activity overview with charts and controls.
"""
import streamlit as st
import plotly.express as px
import plotly.graph_objects as go
import pandas as pd
from datetime import datetime, timedelta, date
from ui_utils import (
fmt_duration, fmt_duration_long, get_focus_score,
privacy_badge, page_header, status_dot, category_pill,
CATEGORY_COLORS, get_db
)
def render():
db = get_db()
privacy_badge()
page_header("Live Dashboard", "Real-time activity overview")
# ── Current Session Card ──────────────────────────────────────────────────
latest = db.get_latest_activity()
tracker_status = db.get_setting("tracker_running", "true")
status_label = {
"true": ("running", "#10b981"),
"paused": ("paused", "#f59e0b"),
"false": ("stopped", "#f43f5e"),
}.get(tracker_status, ("unknown", "#6366f1"))
app_name = latest.get("app_name", "—") if latest else "—"
window = latest.get("window_title", "—") if latest else "—"
category = latest.get("category", "—") if latest else "—"
cat_color = CATEGORY_COLORS.get(category, "#6366f1")
st.markdown(f"""
NOW TRACKING
● {status_label[0].upper()}
{app_name[:60]}
{window[:80]}
{category}
{datetime.now().strftime("%H:%M:%S")}
""", unsafe_allow_html=True)
# ── Summary Metrics ────────────────────────────────────────────────────────
today_start = datetime.combine(date.today(), datetime.min.time())
today_end = datetime.combine(date.today(), datetime.max.time())
summary = db.get_summary(today_start, today_end)
focus_sec = summary.get("focus_seconds") or 0
idle_sec = summary.get("idle_seconds") or 0
total_sec = summary.get("total_seconds") or 0
focus_score = get_focus_score(focus_sec, total_sec)
top_apps = db.get_by_app(today_start, today_end, limit=1)
top_app = top_apps[0]["app_name"] if top_apps else "—"
c1, c2, c3, c4 = st.columns(4)
with c1:
st.metric("🎯 Focus Today", fmt_duration_long(focus_sec),
delta=f"+{fmt_duration(focus_sec)}" if focus_sec > 0 else None)
with c2:
st.metric("💤 Idle Time", fmt_duration_long(idle_sec))
with c3:
pct_color = "normal" if focus_score >= 60 else "inverse"
st.metric("📊 Focus Score", f"{focus_score}%",
delta="Good" if focus_score >= 60 else "Low",
delta_color=pct_color)
with c4:
st.metric("🏆 Top App", top_app[:20] if top_app != "—" else "—")
st.markdown("", unsafe_allow_html=True)
# ── Controls ──────────────────────────────────────────────────────────────
st.markdown("**Tracker Controls**")
col1, col2, col3, col4 = st.columns([1, 1, 1, 4])
with col1:
if st.button("▶ Start", use_container_width=True):
db.set_setting("tracker_running", "true")
st.toast("✅ Tracker started!", icon="▶")
st.rerun()
with col2:
if st.button("⏸ Pause", use_container_width=True):
db.set_setting("tracker_running", "paused")
st.toast("⏸ Tracker paused", icon="⏸")
st.rerun()
with col3:
if st.button("⏹ Stop", use_container_width=True):
db.set_setting("tracker_running", "false")
st.toast("⏹ Tracker stopped", icon="⏹")
st.rerun()
st.markdown("", unsafe_allow_html=True)
# ── Charts Row ────────────────────────────────────────────────────────────
col_left, col_right = st.columns([1.1, 1.9])
with col_left:
st.markdown("#### Time by Category")
cat_data = db.get_by_category(today_start, today_end)
if cat_data:
df_cat = pd.DataFrame(cat_data)
df_cat["minutes"] = df_cat["total_seconds"] / 60
df_cat["label"] = df_cat.apply(
lambda r: f"{r['category']} ({fmt_duration(r['total_seconds'])})", axis=1
)
colors = [CATEGORY_COLORS.get(c, "#6366f1") for c in df_cat["category"]]
fig = go.Figure(go.Pie(
labels=df_cat["category"],
values=df_cat["minutes"],
hole=0.55,
marker=dict(colors=colors, line=dict(width=2, color="#0a0a0f")),
textinfo="label+percent",
textfont=dict(size=11, family="Inter"),
hovertemplate="%{label}
%{value:.0f} min
%{percent}",
))
fig.update_layout(
showlegend=False,
margin=dict(t=20, b=20, l=20, r=20),
height=300,
paper_bgcolor="rgba(0,0,0,0)",
plot_bgcolor="rgba(0,0,0,0)",
font=dict(color="#9090b0", family="Inter"),
annotations=[dict(
text=f"{fmt_duration(focus_sec)}",
x=0.5, y=0.5, font_size=16,
font_family="Syne", font_color="#f0f0ff",
showarrow=False
)]
)
st.plotly_chart(fig, use_container_width=True, config={"displayModeBar": False})
else:
st.markdown("""
No data yet today. Start the tracker to begin.
""", unsafe_allow_html=True)
with col_right:
st.markdown("#### Hourly Activity Timeline")
hourly = db.get_hourly_timeline(date.today())
if hourly:
df_h = pd.DataFrame(hourly)
df_h["hour_label"] = df_h["hour"].apply(lambda h: f"{int(h):02d}:00")
df_h["minutes"] = df_h["total_seconds"] / 60
df_h["color"] = df_h["category"].map(lambda c: CATEGORY_COLORS.get(c, "#6366f1"))
fig2 = px.bar(
df_h, x="hour_label", y="minutes",
color="category",
color_discrete_map=CATEGORY_COLORS,
labels={"hour_label": "Hour", "minutes": "Minutes"},
barmode="stack",
)
fig2.update_layout(
margin=dict(t=20, b=30, l=0, r=0),
height=300,
paper_bgcolor="rgba(0,0,0,0)",
plot_bgcolor="rgba(0,0,0,0)",
font=dict(color="#9090b0", family="Inter"),
xaxis=dict(showgrid=False, tickfont=dict(size=10)),
yaxis=dict(showgrid=True, gridcolor="#2a2a3a", tickfont=dict(size=10)),
legend=dict(
orientation="h", yanchor="bottom", y=1.02,
xanchor="left", x=0, font=dict(size=10)
),
bargap=0.15,
)
st.plotly_chart(fig2, use_container_width=True, config={"displayModeBar": False})
else:
st.markdown("""
No hourly data yet for today.
""", unsafe_allow_html=True)
# ── Top Apps Table ────────────────────────────────────────────────────────
st.markdown("#### Top Apps Today")
top_apps_data = db.get_by_app(today_start, today_end, limit=8)
if top_apps_data:
df_apps = pd.DataFrame(top_apps_data)
df_apps["time"] = df_apps["total_seconds"].apply(fmt_duration_long)
df_apps["pct"] = df_apps["total_seconds"].apply(
lambda s: f"{(s / focus_sec * 100):.0f}%" if focus_sec > 0 else "—"
)
# Render as styled rows
html_rows = ""
for _, row in df_apps.iterrows():
cat = row.get("category", "uncategorized")
color = CATEGORY_COLORS.get(cat, "#6366f1")
bar_w = int((row["total_seconds"] / (df_apps["total_seconds"].max() or 1)) * 100)
html_rows += f"""
| {row['app_name'][:40]} |
{cat}
|
{row['time']} |
|
"""
st.markdown(f"""
| App |
Category |
Time |
Share |
{html_rows}
""", unsafe_allow_html=True)
else:
st.markdown("No app data yet. Start tracking!
",
unsafe_allow_html=True)
# Auto-refresh every 10 seconds
if st.session_state.get("auto_refresh", True):
import time
st.markdown("""
""", unsafe_allow_html=True)