selftracker / settings.py
Nakvi's picture
Upload 14 files
cd7bed1 verified
"""
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"<span style='color:{color}; font-size:1rem; font-weight:600;'>{label}</span>",
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("<p style='color:#9090b0; font-size:0.85rem;'>Define which apps and keywords map to which category. The tracker uses these rules to auto-categorize activity.</p>", 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("<div style='margin-top:28px'></div>", 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(
"<div style='background:#1a0a0f; border:1px solid #4a1a2a; border-radius:12px; padding:1.25rem; margin-bottom:1rem;'>"
"<p style='color:#f43f5e; font-weight:600; margin:0 0 8px;'>⚠️ Clear All Data</p>"
"<p style='color:#9090b0; font-size:0.85rem; margin:0;'>This permanently deletes all tracked activity. This action cannot be undone.</p>"
"</div>",
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("""
<div style="background:#16161f; border:1px solid #2a2a3a; border-radius:12px; padding:1.5rem;">
<h3 style="font-family:'Syne',sans-serif; margin:0 0 8px; color:#f0f0ff;">FocusTrack v1.0</h3>
<p style="color:#9090b0; font-size:0.85rem; margin:0 0 4px;">100% local, privacy-first productivity tracker.</p>
<p style="color:#9090b0; font-size:0.85rem; margin:0 0 4px;">Built with Python 3.11+ Β· Streamlit Β· SQLite Β· Plotly</p>
<p style="color:#5a5a7a; font-size:0.8rem; margin:0;">No internet β€’ No accounts β€’ No cloud β€’ Your data never leaves your device.</p>
</div>
""", unsafe_allow_html=True)