""" FocusTrack - Page 4: Settings Configure tracker behavior, categories, and data management. """ import streamlit as st import json import shutil from datetime import datetime from pathlib import Path from ui_utils import privacy_badge, page_header, CATEGORY_COLORS, get_db DB_PATH = Path(__file__).parent.parent / "data" / "activity.db" def render(): db = get_db() privacy_badge() page_header("Settings", "Configure FocusTrack to your workflow") settings = db.get_all_settings() tab1, tab2, tab3, tab4 = st.tabs(["โ๏ธ Tracker", "๐ท๏ธ Categories", "๐๏ธ Data & Backup", "๐จ Appearance"]) # โโโ Tab 1: Tracker Settings โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ with tab1: st.markdown("#### Tracking Behavior") col1, col2 = st.columns(2) with col1: idle_threshold = st.number_input( "Idle threshold (seconds)", min_value=30, max_value=3600, value=int(settings.get("idle_threshold_seconds", 300)), step=30, help="User is considered idle after this many seconds without mouse/keyboard activity." ) heartbeat = st.number_input( "Heartbeat interval (seconds)", min_value=1, max_value=60, value=int(settings.get("heartbeat_interval", 5)), step=1, help="How often activity is logged. Lower = more granular, higher = less CPU." ) with col2: ignored_raw = settings.get("ignored_apps", '[]') try: ignored_list = json.loads(ignored_raw) except Exception: ignored_list = [] ignored_text = st.text_area( "Ignored apps (one per line)", value="\n".join(ignored_list), height=120, help="Apps listed here will not be tracked." ) auto_start = st.toggle( "Auto-start tracker on launch", value=settings.get("auto_start", "false") == "true" ) if st.button("๐พ Save Tracker Settings", use_container_width=False): db.set_setting("idle_threshold_seconds", str(idle_threshold)) db.set_setting("heartbeat_interval", str(heartbeat)) db.set_setting("auto_start", "true" if auto_start else "false") new_ignored = [a.strip() for a in ignored_text.strip().split("\n") if a.strip()] db.set_setting("ignored_apps", json.dumps(new_ignored)) st.toast("โ Tracker settings saved!", icon="โ ") st.rerun() st.markdown("---") st.markdown("#### Tracker Status") status = db.get_setting("tracker_running", "true") status_map = {"true": ("๐ข Running", "#10b981"), "paused": ("๐ก Paused", "#f59e0b"), "false": ("๐ด Stopped", "#f43f5e")} label, color = status_map.get(status, ("Unknown", "#6366f1")) st.markdown( f"{label}", unsafe_allow_html=True ) col_s1, col_s2, col_s3 = st.columns([1, 1, 1]) with col_s1: if st.button("โถ Start", use_container_width=True): db.set_setting("tracker_running", "true") st.toast("Tracker started") st.rerun() with col_s2: if st.button("โธ Pause", use_container_width=True): db.set_setting("tracker_running", "paused") st.toast("Tracker paused") st.rerun() with col_s3: if st.button("โน Stop", use_container_width=True): db.set_setting("tracker_running", "false") st.toast("Tracker stopped") st.rerun() # โโโ Tab 2: Categories โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ with tab2: st.markdown("#### Category Rules") st.markdown("
Define which apps and keywords map to which category. The tracker uses these rules to auto-categorize activity.
", unsafe_allow_html=True) cats = db.get_categories() for cat in cats: with st.expander(f"{'โ'} {cat['name'].title()}", expanded=False): col_n, col_c = st.columns([3, 1]) with col_n: cat_name = st.text_input("Name", value=cat["name"], key=f"cname_{cat['id']}") with col_c: cat_color = st.color_picker("Color", value=cat["color"], key=f"ccolor_{cat['id']}") try: kw_list = json.loads(cat["keywords"] or "[]") app_list = json.loads(cat["apps"] or "[]") except Exception: kw_list, app_list = [], [] col_k, col_a = st.columns(2) with col_k: kw_text = st.text_area( "Keywords (one per line)", value="\n".join(kw_list), height=100, key=f"kw_{cat['id']}", help="These words are matched against app name and window title." ) with col_a: app_text = st.text_area( "App names (one per line)", value="\n".join(app_list), height=100, key=f"apps_{cat['id']}", help="Partial matches against the process/app name." ) col_save, col_del = st.columns([1, 1]) with col_save: if st.button("๐พ Save", key=f"save_cat_{cat['id']}", use_container_width=True): new_kw = [k.strip() for k in kw_text.strip().split("\n") if k.strip()] new_app = [a.strip() for a in app_text.strip().split("\n") if a.strip()] db.upsert_category(cat_name, cat_color, new_kw, new_app) st.toast(f"โ '{cat_name}' saved") st.rerun() with col_del: if cat["name"] not in ("idle", "uncategorized"): if st.button("๐ Delete", key=f"del_cat_{cat['id']}", use_container_width=True): db.delete_category(cat["name"]) st.toast(f"Deleted '{cat['name']}'") st.rerun() st.markdown("---") st.markdown("#### Add New Category") nc1, nc2, nc3 = st.columns([2, 1, 1]) with nc1: new_cat_name = st.text_input("Category name", placeholder="e.g. learning") with nc2: new_cat_color = st.color_picker("Color", value="#6366f1") with nc3: st.markdown("", unsafe_allow_html=True) if st.button("โ Add Category", use_container_width=True): if new_cat_name.strip(): db.upsert_category(new_cat_name.strip(), new_cat_color, [], []) st.toast(f"โ Added '{new_cat_name}'") st.rerun() # โโโ Tab 3: Data & Backup โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ with tab3: st.markdown("#### Database Info") db_size = db.get_db_size_mb() from datetime import date from datetime import datetime as dt2 from ui_utils import get_date_range start, end = get_date_range("Last 30 days") count = db.get_activity_count(start, end) col_i1, col_i2, col_i3 = st.columns(3) with col_i1: st.metric("Database Size", f"{db_size:.2f} MB") with col_i2: st.metric("Records (30d)", f"{count:,}") with col_i3: st.metric("DB Location", "Local only โ ") st.code(str(DB_PATH), language=None) st.markdown("---") st.markdown("#### Backup Database") col_b1, col_b2 = st.columns([1, 2]) with col_b1: if st.button("๐ฆ Download Backup", use_container_width=True): if DB_PATH.exists(): with open(DB_PATH, "rb") as f: data = f.read() st.download_button( "๐พ Save activity.db", data, file_name=f"focustrack_backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}.db", mime="application/octet-stream", use_container_width=True, ) st.markdown("---") st.markdown("#### Restore Database") uploaded = st.file_uploader("Upload a backup .db file", type=["db"]) if uploaded: if st.button("โ ๏ธ Restore from backup (overwrites current data)"): DB_PATH.write_bytes(uploaded.read()) if "db" in st.session_state: del st.session_state["db"] st.toast("โ Database restored! Please refresh the page.", icon="โ ") st.markdown("---") st.markdown("#### Danger Zone") st.markdown( "โ ๏ธ Clear All Data
" "This permanently deletes all tracked activity. This action cannot be undone.
" "100% local, privacy-first productivity tracker.
Built with Python 3.11+ ยท Streamlit ยท SQLite ยท Plotly
No internet โข No accounts โข No cloud โข Your data never leaves your device.