Spaces:
Sleeping
Sleeping
| """ | |
| FocusTrack - Page 3: Activity Log | |
| Searchable, filterable, paginated activity table. | |
| """ | |
| import streamlit as st | |
| import pandas as pd | |
| from datetime import datetime, timedelta, date | |
| from ui_utils import ( | |
| fmt_duration, privacy_badge, page_header, | |
| CATEGORY_COLORS, get_db, get_date_range | |
| ) | |
| PAGE_SIZE = 50 | |
| def render(): | |
| db = get_db() | |
| privacy_badge() | |
| page_header("Activity Log", "Browse and search all tracked sessions") | |
| # โโ Filters โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| col1, col2, col3, col4 = st.columns([1.5, 1, 1, 1]) | |
| with col1: | |
| search = st.text_input("๐ Search", placeholder="App, title, or categoryโฆ") | |
| with col2: | |
| range_sel = st.selectbox("Range", ["Today", "Yesterday", "Last 7 days", "Last 30 days", "Custom"], index=2) | |
| start, end = get_date_range(range_sel) | |
| if range_sel == "Custom": | |
| with col3: | |
| start_d = st.date_input("From", value=date.today() - timedelta(days=6)) | |
| with col4: | |
| end_d = st.date_input("To", value=date.today()) | |
| start = datetime.combine(start_d, datetime.min.time()) | |
| end = datetime.combine(end_d, datetime.max.time()) | |
| # Filter by category | |
| cats = [c["name"] for c in db.get_categories()] | |
| with col3 if range_sel != "Custom" else st.columns(1)[0]: | |
| pass | |
| cat_filter_col, show_idle_col = st.columns([2, 1]) | |
| with cat_filter_col: | |
| selected_cats = st.multiselect("Filter by category", cats, default=[]) | |
| with show_idle_col: | |
| show_idle = st.checkbox("Include idle", value=False) | |
| # โโ Count & Pagination โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| total_count = db.get_activity_count(start, end, search) | |
| if "log_page" not in st.session_state: | |
| st.session_state.log_page = 0 | |
| page = st.session_state.log_page | |
| offset = page * PAGE_SIZE | |
| total_pages = max(1, (total_count + PAGE_SIZE - 1) // PAGE_SIZE) | |
| # โโ Fetch Data โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| rows = db.get_activities(start, end, search=search, limit=PAGE_SIZE, offset=offset) | |
| # Apply local filters | |
| if selected_cats: | |
| rows = [r for r in rows if r.get("category") in selected_cats] | |
| if not show_idle: | |
| rows = [r for r in rows if not r.get("is_idle")] | |
| # โโ Stats bar โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| st.markdown( | |
| f"<p style='color:#9090b0; font-size:0.8rem; margin:8px 0;'>" | |
| f"Showing {len(rows)} of {total_count:,} records | " | |
| f"Page {page + 1} of {total_pages}" | |
| f"</p>", | |
| unsafe_allow_html=True, | |
| ) | |
| # โโ Table โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| if rows: | |
| html_rows = "" | |
| for r in rows: | |
| cat = r.get("category", "uncategorized") | |
| color = CATEGORY_COLORS.get(cat, "#6366f1") | |
| ts = r.get("timestamp", "")[:19].replace("T", " ") | |
| is_idle_icon = "๐ค" if r.get("is_idle") else "โ" | |
| idle_color = "#374151" if r.get("is_idle") else "#10b981" | |
| dur = fmt_duration(r.get("duration_seconds", 0)) | |
| app = str(r.get("app_name", ""))[:30] | |
| title = str(r.get("window_title", ""))[:60] | |
| html_rows += f""" | |
| <tr style="border-bottom:1px solid #1a1a2a; transition:background 0.15s;" | |
| onmouseover="this.style.background='#1c1c27'" onmouseout="this.style.background='transparent'"> | |
| <td style="padding:9px 12px; font-family:'DM Mono',monospace; font-size:0.78rem; color:#9090b0; white-space:nowrap;">{ts}</td> | |
| <td style="padding:9px 12px; font-weight:500; color:#f0f0ff; font-size:0.85rem;">{app}</td> | |
| <td style="padding:9px 12px; color:#9090b0; font-size:0.82rem; max-width:280px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">{title}</td> | |
| <td style="padding:9px 12px; font-family:'DM Mono',monospace; font-size:0.82rem; color:#9090b0;">{dur}</td> | |
| <td style="padding:9px 12px;"> | |
| <span style="background:{color}22; color:{color}; border:1px solid {color}44; | |
| border-radius:20px; padding:2px 8px; font-size:0.7rem; font-weight:600; font-family:'DM Mono',monospace;">{cat}</span> | |
| </td> | |
| <td style="padding:9px 12px; color:{idle_color}; font-size:0.9rem;" title="{'Idle' if r.get('is_idle') else 'Active'}">{is_idle_icon}</td> | |
| </tr>""" | |
| st.markdown(f""" | |
| <div style="overflow-x:auto; border:1px solid #2a2a3a; border-radius:12px; margin-bottom:1rem;"> | |
| <table style="width:100%; border-collapse:collapse; font-family:'Inter',sans-serif;"> | |
| <thead> | |
| <tr style="background:#111118; color:#5a5a7a; font-size:0.7rem; text-transform:uppercase; letter-spacing:0.08em; border-bottom:1px solid #2a2a3a;"> | |
| <th style="text-align:left; padding:10px 12px; font-weight:500;">Time</th> | |
| <th style="text-align:left; padding:10px 12px; font-weight:500;">App</th> | |
| <th style="text-align:left; padding:10px 12px; font-weight:500;">Window Title</th> | |
| <th style="text-align:left; padding:10px 12px; font-weight:500;">Duration</th> | |
| <th style="text-align:left; padding:10px 12px; font-weight:500;">Category</th> | |
| <th style="text-align:left; padding:10px 12px; font-weight:500;">Status</th> | |
| </tr> | |
| </thead> | |
| <tbody>{html_rows}</tbody> | |
| </table> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| else: | |
| st.markdown(""" | |
| <div style="padding:3rem; text-align:center; border:1px dashed #2a2a3a; border-radius:12px; color:#5a5a7a;"> | |
| No activity records found for the selected filters. | |
| </div>""", unsafe_allow_html=True) | |
| # โโ Pagination controls โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| pc1, pc2, pc3, pc4, pc5 = st.columns([1, 1, 2, 1, 1]) | |
| with pc1: | |
| if st.button("โฎ First") and page > 0: | |
| st.session_state.log_page = 0 | |
| st.rerun() | |
| with pc2: | |
| if st.button("โ Prev") and page > 0: | |
| st.session_state.log_page = page - 1 | |
| st.rerun() | |
| with pc3: | |
| st.markdown( | |
| f"<div style='text-align:center; color:#9090b0; font-size:0.82rem; padding-top:8px;'>" | |
| f"Page {page + 1} / {total_pages}</div>", | |
| unsafe_allow_html=True, | |
| ) | |
| with pc4: | |
| if st.button("Next โถ") and page < total_pages - 1: | |
| st.session_state.log_page = page + 1 | |
| st.rerun() | |
| with pc5: | |
| if st.button("Last โญ") and page < total_pages - 1: | |
| st.session_state.log_page = total_pages - 1 | |
| st.rerun() | |
| # โโ Export โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| st.markdown("---") | |
| all_rows = db.get_activities(start, end, search=search, limit=100000) | |
| if all_rows: | |
| df_export = pd.DataFrame(all_rows) | |
| col_ex1, col_ex2 = st.columns([1, 1]) | |
| with col_ex1: | |
| st.download_button( | |
| "๐ฅ Export visible data (CSV)", | |
| df_export.to_csv(index=False).encode("utf-8"), | |
| file_name="focustrack_log.csv", | |
| mime="text/csv", | |
| use_container_width=True, | |
| ) | |
| with col_ex2: | |
| st.download_button( | |
| "๐ฅ Export visible data (JSON)", | |
| df_export.to_json(orient="records", indent=2).encode("utf-8"), | |
| file_name="focustrack_log.json", | |
| mime="application/json", | |
| use_container_width=True, | |
| ) | |