Spaces:
Sleeping
Sleeping
| """ | |
| 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) | |