""" 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.

" "
", unsafe_allow_html=True, ) confirm = st.text_input("Type DELETE to confirm") if st.button("๐Ÿ—‘ Clear All Activity Data", type="secondary"): if confirm == "DELETE": db.clear_all_data() st.toast("All data cleared.") st.rerun() else: st.error("Please type DELETE to confirm.") # โ”€โ”€โ”€ Tab 4: Appearance โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ with tab4: st.markdown("#### Theme") current_theme = db.get_setting("theme", "dark") theme = st.radio( "Color mode", ["dark", "light"], index=0 if current_theme == "dark" else 1, horizontal=True, ) if theme != current_theme: db.set_setting("theme", theme) st.toast("Theme updated โ€” refresh to apply.") st.rerun() st.markdown("---") st.markdown("#### About FocusTrack") st.markdown("""

FocusTrack v1.0

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.

""", unsafe_allow_html=True)