From a0567d5d7e8b622cddda16c936cce0e1293566f0 Mon Sep 17 00:00:00 2001 From: Floze <88098863+floze-the-genius@users.noreply.github.com> Date: Sat, 18 Jul 2026 16:28:00 +0400 Subject: [PATCH 1/3] [Web] Fix report session truncation --- termstory/web.py | 26 +++++++++++++++++------- tests/test_web.py | 52 ++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 70 insertions(+), 8 deletions(-) diff --git a/termstory/web.py b/termstory/web.py index 37c4fd6..8bb2b9f 100644 --- a/termstory/web.py +++ b/termstory/web.py @@ -10,6 +10,9 @@ logger = logging.getLogger(__name__) +WEB_REPORT_SESSION_LIMIT = 500 +AI_HIGHLIGHT_LIMIT = 15 + def get_web_data(db: Database, start_ts: Optional[int] = None, end_ts: Optional[int] = None) -> dict: """Gather stats, project list, timeline, and AI highlights from the database, filtering by date range if provided.""" @@ -58,10 +61,9 @@ def get_web_data(db: Database, start_ts: Optional[int] = None, end_ts: Optional[ if conditions: query += " WHERE " + " AND ".join(conditions) - query += " ORDER BY start_time DESC LIMIT 500" - else: - query += " ORDER BY start_time DESC LIMIT 500" - + query += " ORDER BY start_time DESC LIMIT ?" + params.append(WEB_REPORT_SESSION_LIMIT) + cursor.execute(query, params) session_ids = [row[0] for row in cursor.fetchall()] finally: @@ -152,7 +154,7 @@ def get_web_data(db: Database, start_ts: Optional[int] = None, end_ts: Optional[ # 4. AI Summary Highlights ai_sessions = [s for s in sessions_data if s["ai_summary"]] - if len(ai_sessions) < 15: + if len(ai_sessions) < AI_HIGHLIGHT_LIMIT: # Fetch more from DB if we don't have enough in the filtered set conn = db.get_connection() try: @@ -170,7 +172,8 @@ def get_web_data(db: Database, start_ts: Optional[int] = None, end_ts: Optional[ params_ai.append(end_ts) if conditions_ai: query_ai += " AND " + " AND ".join(conditions_ai) - query_ai += " ORDER BY start_time DESC LIMIT 15" + query_ai += " ORDER BY start_time DESC LIMIT ?" + params_ai.append(AI_HIGHLIGHT_LIMIT) cursor.execute(query_ai, params_ai) extra_ids = [row[0] for row in cursor.fetchall()] @@ -200,7 +203,7 @@ def get_web_data(db: Database, start_ts: Optional[int] = None, end_ts: Optional[ }) ai_sessions.sort(key=lambda x: x["start_time"], reverse=True) - highlights_data = ai_sessions[:15] + highlights_data = ai_sessions[:AI_HIGHLIGHT_LIMIT] # 5. Calculate daily activity for the last 90 days for the heatmap now_dt = datetime.now() @@ -242,10 +245,19 @@ def get_web_data(db: Database, start_ts: Optional[int] = None, end_ts: Optional[ finally: conn.close() + total_sessions = stats["total_sessions"] + session_window = { + "limit": WEB_REPORT_SESSION_LIMIT, + "returned": len(sessions_data), + "total": total_sessions, + "truncated": total_sessions > len(sessions_data), + } + return { "stats": stats, "projects": projects_data, "sessions": sessions_data, + "session_window": session_window, "highlights": highlights_data, "daily_activity": daily_activity } diff --git a/tests/test_web.py b/tests/test_web.py index 732edd4..4decafd 100644 --- a/tests/test_web.py +++ b/tests/test_web.py @@ -28,6 +28,12 @@ def test_get_web_data_empty_db(tmp_path): assert data["stats"]["streak"] == 0 assert len(data["projects"]) == 0 assert len(data["sessions"]) == 0 + assert data["session_window"] == { + "limit": 1000, + "returned": 0, + "total": 0, + "truncated": False, + } assert len(data["highlights"]) == 0 @@ -152,6 +158,44 @@ def test_get_web_data_populated_db(tmp_path): assert data["highlights"][0]["project_name"] == "Project A" assert data["highlights"][0]["ai_summary"] == "Started project A" + +def test_get_web_data_does_not_truncate_unfiltered_sessions_at_30(tmp_path): + db = Database(str(tmp_path / "sessions.db")) + db.init_db() + + now = int(datetime(2026, 6, 14, 12, 0, 0).timestamp()) + project = Project( + id=1, + name="Project A", + path="/path/to/a", + first_seen=now, + last_seen=now, + session_count=31, + total_time=31, + ) + sessions = [ + Session( + id=index + 1, + start_time=now + index, + end_time=now + index + 1, + duration_seconds=1, + project_id=1, + ) + for index in range(31) + ] + db.save_data([project], sessions, []) + + data = get_web_data(db) + + assert len(data["sessions"]) == 31 + assert data["session_window"] == { + "limit": 1000, + "returned": 31, + "total": 31, + "truncated": False, + } + + def test_generate_and_open_report(tmp_path, monkeypatch): db_file = tmp_path / "report.db" db = Database(str(db_file)) @@ -292,6 +336,13 @@ def test_swarm_audit_fixes(tmp_path, monkeypatch): assert data["stats"]["total_sessions"] == 1005 assert data["stats"]["total_commands"] == 1005 assert data["stats"]["total_projects"] == 1 + assert len(data["sessions"]) == 1000 + assert data["session_window"] == { + "limit": 1000, + "returned": 1000, + "total": 1005, + "truncated": True, + } # Verify daily activity heatmap calculations work and are populated today_str = datetime.fromtimestamp(now).strftime("%Y-%m-%d") @@ -321,4 +372,3 @@ def test_swarm_audit_fixes(tmp_path, monkeypatch): assert "const reportData =" in html # Check if our backslash command exists intact in the JSON assert "backslash \\\\" in html - From 1e3811a87be0d9380c339dab4475bea84a32492f Mon Sep 17 00:00:00 2001 From: Floze <88098863+floze-the-genius@users.noreply.github.com> Date: Sat, 18 Jul 2026 16:43:21 +0400 Subject: [PATCH 2/3] test(web): share report session limit --- tests/test_web.py | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/tests/test_web.py b/tests/test_web.py index 4decafd..81dcfce 100644 --- a/tests/test_web.py +++ b/tests/test_web.py @@ -8,7 +8,7 @@ from termstory.cli import app from termstory.database import Database from termstory.models import Project, Session, Command -from termstory.web import get_web_data, generate_and_open_report +from termstory.web import WEB_REPORT_SESSION_LIMIT, get_web_data, generate_and_open_report def test_get_web_data_empty_db(tmp_path): db_file = tmp_path / "empty.db" @@ -29,7 +29,7 @@ def test_get_web_data_empty_db(tmp_path): assert len(data["projects"]) == 0 assert len(data["sessions"]) == 0 assert data["session_window"] == { - "limit": 1000, + "limit": WEB_REPORT_SESSION_LIMIT, "returned": 0, "total": 0, "truncated": False, @@ -189,7 +189,7 @@ def test_get_web_data_does_not_truncate_unfiltered_sessions_at_30(tmp_path): assert len(data["sessions"]) == 31 assert data["session_window"] == { - "limit": 1000, + "limit": WEB_REPORT_SESSION_LIMIT, "returned": 31, "total": 31, "truncated": False, @@ -312,13 +312,13 @@ def test_swarm_audit_fixes(tmp_path, monkeypatch): db = Database(str(db_file)) db.init_db() - # We will insert more than 1000 sessions (e.g. 1005 sessions) to check uncapped override + session_count = WEB_REPORT_SESSION_LIMIT + 5 now = int(datetime(2026, 6, 14, 12, 0, 0).timestamp()) - projects = [Project(id=1, name="Project A", path="/path/to/a", first_seen=now, last_seen=now, session_count=1005, total_time=10050)] + projects = [Project(id=1, name="Project A", path="/path/to/a", first_seen=now, last_seen=now, session_count=session_count, total_time=session_count * 10)] sessions = [] commands = [] - for i in range(1005): + for i in range(session_count): s_id = i + 1 s_time = now - i * 10 # spread out in time sessions.append(Session(id=s_id, start_time=s_time, end_time=s_time + 5, duration_seconds=5, project_id=1)) @@ -332,24 +332,24 @@ def test_swarm_audit_fixes(tmp_path, monkeypatch): start_ts = now - 20000 data = get_web_data(db, start_ts=start_ts) - # Verify KPI stats override does NOT cap at 1000 - assert data["stats"]["total_sessions"] == 1005 - assert data["stats"]["total_commands"] == 1005 + # Verify KPI stats override does not cap at the report session limit. + assert data["stats"]["total_sessions"] == session_count + assert data["stats"]["total_commands"] == session_count assert data["stats"]["total_projects"] == 1 - assert len(data["sessions"]) == 1000 + assert len(data["sessions"]) == WEB_REPORT_SESSION_LIMIT assert data["session_window"] == { - "limit": 1000, - "returned": 1000, - "total": 1005, + "limit": WEB_REPORT_SESSION_LIMIT, + "returned": WEB_REPORT_SESSION_LIMIT, + "total": session_count, "truncated": True, } # Verify daily activity heatmap calculations work and are populated today_str = datetime.fromtimestamp(now).strftime("%Y-%m-%d") assert today_str in data["daily_activity"] - # The sum of commands across all days in heatmap should be 1005 + # The sum of commands across all days in heatmap should include every session. total_heatmap_commands = sum(day["commands"] for day in data["daily_activity"].values()) - assert total_heatmap_commands == 1005 + assert total_heatmap_commands == session_count # Test custom template with const reportData = ... and backslashes template_file = tmp_path / "custom_template.html" From 90dea0bee76045b86bf599237b24d3299d47be33 Mon Sep 17 00:00:00 2001 From: Floze <88098863+floze-the-genius@users.noreply.github.com> Date: Sat, 18 Jul 2026 18:21:12 +0400 Subject: [PATCH 3/3] fix(web): surface truncated session reports --- termstory/web.py | 21 +++++++++++++++++++++ tests/test_web.py | 3 +++ 2 files changed, 24 insertions(+) diff --git a/termstory/web.py b/termstory/web.py index 8bb2b9f..d07709f 100644 --- a/termstory/web.py +++ b/termstory/web.py @@ -480,6 +480,18 @@ def generate_and_open_report( color: var(--text-secondary); font-size: 0.9rem; }} + .session-window-notice {{ + display: none; + margin: -16px 0 28px; + padding: 12px 16px; + border-left: 3px solid var(--accent-color); + background: var(--panel-bg); + color: var(--text-secondary); + font-size: 0.9rem; + }} + .session-window-notice.visible {{ + display: block; + }} /* Glassmorphism panel */ .panel {{ @@ -851,6 +863,8 @@ def generate_and_open_report(
Last Ingested: -
+ +
@@ -1179,6 +1193,13 @@ def generate_and_open_report( document.getElementById('kpi-commands').textContent = reportData.stats.total_commands; document.getElementById('kpi-projects').textContent = reportData.stats.total_projects; document.getElementById('kpi-streak').textContent = `${{reportData.stats.streak}} Days`; + + const sessionWindow = reportData.session_window; + if (sessionWindow?.truncated) {{ + const notice = document.getElementById('session-window-notice'); + notice.textContent = `Showing ${{sessionWindow.returned}} of ${{sessionWindow.total}} sessions.`; + notice.classList.add('visible'); + }} const searchInput = document.getElementById('search-bar'); searchInput.addEventListener('input', (e) => {{ diff --git a/tests/test_web.py b/tests/test_web.py index 81dcfce..bdaeb75 100644 --- a/tests/test_web.py +++ b/tests/test_web.py @@ -227,6 +227,9 @@ def test_generate_and_open_report(tmp_path, monkeypatch): assert "TermStory Web Report" in html assert "const reportData = " in html + assert 'id="session-window-notice"' in html + assert "if (sessionWindow?.truncated)" in html + assert "Showing ${sessionWindow.returned} of ${sessionWindow.total} sessions." in html def test_cli_web_subcommand(tmp_path, monkeypatch): db_file = tmp_path / "cli_web.db"