selftracker / activity_log.py
Nakvi's picture
Upload 14 files
cd7bed1 verified
"""
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 &nbsp;|&nbsp; "
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,
)