* docs: add 5 sidebar icon color concepts for UX review Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(ui): add semantic icon colors and updated icons to sidebar nav Swap generic icons for more descriptive alternatives (Network, Wrench, FileOutput, Library, Code2, Lightbulb) and assign each nav item a unique semantic color for instant visual landmarks. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(ui): default Sessions page to Active tab, reorder tabs Active sessions are what engineers care about most. Tab order is now Active, Prepared, Completed, All. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs: add sidebar grouping and AI naming concept mockups Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs: add sidebar redesign context and decision summary Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs: add sidebar redesign spec and implementation plan Design spec covers: activity zone with daily stats + session feed, nav grouping (Resolve/Build/Insights), AI split (FlowPilot + Flow Assist), pinned flows removal. Implementation plan has 5 chunks, 12 tasks, 39 steps. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: add sidebar stats Pydantic schemas Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * test: add failing tests for sidebar stats endpoint Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: add sidebar stats endpoint with daily stats and activity feed Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: add sidebar API client, stats bar, activity feed components New components: SidebarStatsBar, SidebarActivityFeed, ActivityItem. New API client for sidebar stats endpoint. Pulse-dot CSS animation. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: restructure sidebar with stats bar, activity feed, and grouped nav Dashboard-first layout with Resolve/Build/Insights groups. AI split: FlowPilot (Resolve) + Flow Assist (Build). Stats bar: Resolved/Active/In Session daily counters. Activity feed: active sessions with CW ticket #, recent completions. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor: remove pinned flows frontend (PinnedFlowsSection, store, API, pin buttons) Removed: PinnedFlowsSection component, pinnedFlowsStore, pinnedFlows API client. Cleaned: pin buttons from TreeGridView, TreeListView, TreeTableView. Cleaned: favorites section from QuickStartPage, pin props from TreeLibraryPage. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: add FlowAssistPage placeholder and /flow-assist route Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: real-time sidebar stats via session-changed events Sidebar now refreshes stats when sessions are created or completed, not just on page navigation. Uses window event bus pattern (same as folder-changed events in codebase). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: live-ticking In Session timer using active session start times SidebarStatsBar now computes active session elapsed time client-side from started_at timestamps, ticking every 60s. Backend only returns completed session minutes to avoid double-counting. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: sidebar In Session timer ticks every second and shows seconds Timer now uses 1s interval (not 60s) and displays seconds when under a minute so it matches the session timer in the flow UI. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore: trigger PR environment redeploy Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * debug: add console.log to SidebarStatsBar for timer investigation Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: parse sidebar timestamps as UTC (append Z suffix) Backend returns naive UTC timestamps without timezone indicator. JS Date() treats bare ISO strings as local time, causing the timer to compute negative elapsed time (future timestamps). Appending 'Z' forces UTC parsing. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: rename 'In Session' to 'Total Time' for clarity Makes it clear the timer is an aggregate of all sessions today, not just the current one. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
143 lines
4.9 KiB
Python
143 lines
4.9 KiB
Python
"""Integration tests for sidebar stats endpoint."""
|
|
|
|
import pytest
|
|
from httpx import AsyncClient
|
|
|
|
|
|
class TestSidebarStats:
|
|
"""Tests for GET /sessions/sidebar-stats."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_sidebar_stats_no_sessions(
|
|
self, client: AsyncClient, auth_headers: dict
|
|
):
|
|
"""Empty stats when user has no sessions."""
|
|
response = await client.get(
|
|
"/api/v1/sessions/sidebar-stats?tz_offset=0",
|
|
headers=auth_headers,
|
|
)
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["resolved_today"] == 0
|
|
assert data["active_count"] == 0
|
|
assert data["total_session_minutes_today"] == 0
|
|
assert data["active_sessions"] == []
|
|
assert data["recent_completions"] == []
|
|
assert "tree_counts" in data
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_sidebar_stats_with_active_session(
|
|
self, client: AsyncClient, auth_headers: dict, test_tree: dict
|
|
):
|
|
"""Active session appears in activity feed."""
|
|
create_resp = await client.post(
|
|
"/api/v1/sessions",
|
|
json={"tree_id": test_tree["id"], "ticket_number": "TK-100"},
|
|
headers=auth_headers,
|
|
)
|
|
assert create_resp.status_code == 201
|
|
|
|
response = await client.get(
|
|
"/api/v1/sessions/sidebar-stats?tz_offset=0",
|
|
headers=auth_headers,
|
|
)
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["active_count"] == 1
|
|
assert len(data["active_sessions"]) == 1
|
|
assert data["active_sessions"][0]["ticket_number"] == "TK-100"
|
|
assert data["active_sessions"][0]["tree_id"] == test_tree["id"]
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_sidebar_stats_resolved_today(
|
|
self, client: AsyncClient, auth_headers: dict, test_tree: dict
|
|
):
|
|
"""Resolved session counts in resolved_today."""
|
|
create_resp = await client.post(
|
|
"/api/v1/sessions",
|
|
json={"tree_id": test_tree["id"]},
|
|
headers=auth_headers,
|
|
)
|
|
session_id = create_resp.json()["id"]
|
|
|
|
# Complete with resolved outcome
|
|
await client.post(
|
|
f"/api/v1/sessions/{session_id}/complete",
|
|
json={"outcome": "resolved", "outcome_notes": "Fixed it"},
|
|
headers=auth_headers,
|
|
)
|
|
|
|
response = await client.get(
|
|
"/api/v1/sessions/sidebar-stats?tz_offset=0",
|
|
headers=auth_headers,
|
|
)
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["resolved_today"] >= 1
|
|
assert data["active_count"] == 0
|
|
assert len(data["recent_completions"]) >= 1
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_sidebar_stats_max_active_sessions(
|
|
self, client: AsyncClient, auth_headers: dict, test_tree: dict
|
|
):
|
|
"""Active sessions capped at 5."""
|
|
for i in range(7):
|
|
await client.post(
|
|
"/api/v1/sessions",
|
|
json={"tree_id": test_tree["id"], "ticket_number": f"TK-{i}"},
|
|
headers=auth_headers,
|
|
)
|
|
|
|
response = await client.get(
|
|
"/api/v1/sessions/sidebar-stats?tz_offset=0",
|
|
headers=auth_headers,
|
|
)
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["active_count"] == 7
|
|
assert len(data["active_sessions"]) == 5
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_sidebar_stats_recent_completions_max_3(
|
|
self, client: AsyncClient, auth_headers: dict, test_tree: dict
|
|
):
|
|
"""Recent completions capped at 3."""
|
|
for i in range(5):
|
|
create_resp = await client.post(
|
|
"/api/v1/sessions",
|
|
json={"tree_id": test_tree["id"]},
|
|
headers=auth_headers,
|
|
)
|
|
session_id = create_resp.json()["id"]
|
|
await client.post(
|
|
f"/api/v1/sessions/{session_id}/complete",
|
|
json={"outcome": "resolved", "outcome_notes": "Done"},
|
|
headers=auth_headers,
|
|
)
|
|
|
|
response = await client.get(
|
|
"/api/v1/sessions/sidebar-stats?tz_offset=0",
|
|
headers=auth_headers,
|
|
)
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert len(data["recent_completions"]) == 3
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_sidebar_stats_requires_auth(self, client: AsyncClient):
|
|
"""Endpoint requires authentication."""
|
|
response = await client.get("/api/v1/sessions/sidebar-stats?tz_offset=0")
|
|
assert response.status_code == 401
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_sidebar_stats_requires_tz_offset(
|
|
self, client: AsyncClient, auth_headers: dict
|
|
):
|
|
"""tz_offset query param is required."""
|
|
response = await client.get(
|
|
"/api/v1/sessions/sidebar-stats",
|
|
headers=auth_headers,
|
|
)
|
|
assert response.status_code == 422
|