selftracker / dashboard.py
Nakvi's picture
Upload 14 files
cd7bed1 verified
"""
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"""
<div style="
background: linear-gradient(135deg, #16161f 0%, #1a1a2a 100%);
border: 1px solid #2a2a3a; border-left: 4px solid {cat_color};
border-radius: 16px; padding: 1.5rem 2rem; margin-bottom: 1.5rem;
display: flex; justify-content: space-between; align-items: center;
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
">
<div>
<div style="display:flex; align-items:center; gap:10px; margin-bottom:6px;">
<span style="font-size:0.75rem; color:#9090b0; font-family:'DM Mono',monospace; text-transform:uppercase; letter-spacing:0.1em;">NOW TRACKING</span>
<span style="background:{status_label[1]}22; color:{status_label[1]}; border:1px solid {status_label[1]}44; border-radius:20px; padding:2px 10px; font-size:0.7rem; font-weight:600;">● {status_label[0].upper()}</span>
</div>
<div style="font-family:'Syne',sans-serif; font-size:1.6rem; font-weight:700; color:#f0f0ff; margin-bottom:4px; letter-spacing:-0.02em;">
{app_name[:60]}
</div>
<div style="color:#9090b0; font-size:0.85rem; margin-bottom:8px; max-width:600px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">
{window[:80]}
</div>
<span style="background:{cat_color}22; color:{cat_color}; border:1px solid {cat_color}44; border-radius:20px; padding:3px 12px; font-size:0.72rem; font-weight:600; font-family:'DM Mono',monospace;">
{category}
</span>
</div>
<div style="text-align:right; color:#9090b0; font-family:'DM Mono',monospace; font-size:0.8rem;">
{datetime.now().strftime("%H:%M:%S")}
</div>
</div>
""", 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("<div style='height:1rem'></div>", 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("<div style='height:0.5rem'></div>", 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="<b>%{label}</b><br>%{value:.0f} min<br>%{percent}<extra></extra>",
))
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"<b>{fmt_duration(focus_sec)}</b>",
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("""
<div style="height:280px; display:flex; align-items:center; justify-content:center;
color:#5a5a7a; font-size:0.9rem; border:1px dashed #2a2a3a; border-radius:12px;">
No data yet today. Start the tracker to begin.
</div>""", 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("""
<div style="height:280px; display:flex; align-items:center; justify-content:center;
color:#5a5a7a; font-size:0.9rem; border:1px dashed #2a2a3a; border-radius:12px;">
No hourly data yet for today.
</div>""", 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"""
<tr style="border-bottom:1px solid #1e1e2e;">
<td style="padding:10px 12px; font-weight:500; color:#f0f0ff;">{row['app_name'][:40]}</td>
<td style="padding:10px 12px;">
<span style="background:{color}22; color:{color}; border:1px solid {color}44;
border-radius:20px; padding:2px 8px; font-size:0.7rem; font-family:'DM Mono',monospace;">{cat}</span>
</td>
<td style="padding:10px 12px; font-family:'DM Mono',monospace; font-size:0.85rem;">{row['time']}</td>
<td style="padding:10px 24px 10px 12px; min-width:120px;">
<div style="background:#1e1e2e; border-radius:4px; height:6px; overflow:hidden;">
<div style="background:{color}; width:{bar_w}%; height:100%; border-radius:4px;"></div>
</div>
</td>
</tr>"""
st.markdown(f"""
<table style="width:100%; border-collapse:collapse; font-size:0.85rem; font-family:'Inter',sans-serif;">
<thead>
<tr style="color:#5a5a7a; font-size:0.72rem; text-transform:uppercase; letter-spacing:0.08em; border-bottom:1px solid #2a2a3a;">
<th style="text-align:left; padding:8px 12px; font-weight:500;">App</th>
<th style="text-align:left; padding:8px 12px; font-weight:500;">Category</th>
<th style="text-align:left; padding:8px 12px; font-weight:500;">Time</th>
<th style="text-align:left; padding:8px 24px 8px 12px; font-weight:500;">Share</th>
</tr>
</thead>
<tbody>{html_rows}</tbody>
</table>
""", unsafe_allow_html=True)
else:
st.markdown("<p style='color:#5a5a7a; font-size:0.85rem;'>No app data yet. Start tracking!</p>",
unsafe_allow_html=True)
# Auto-refresh every 10 seconds
if st.session_state.get("auto_refresh", True):
import time
st.markdown("""
<script>
setTimeout(() => window.location.reload(), 15000);
</script>""", unsafe_allow_html=True)