11"""Project listing endpoints."""
22
3- from typing import cast
3+ from flask import Blueprint , current_app
44
5- from flask import Blueprint , current_app , jsonify
6-
7- from api ._flask_types import FlaskReturn , json_ok
8- from models .project import ProjectSessionRowDict
5+ from api ._flask_types import FlaskReturn , json_error , json_response
6+ from models .project import ProjectSessionRowDict , SessionListItemDict
7+ from models .session import SessionDict
98from utils .session_path import get_claude_projects_dir , list_projects , list_sessions , safe_join
109from utils .exclusion_rules import is_session_excluded
1110
1211projects_bp = Blueprint ("projects" , __name__ )
1312
1413
14+ def _session_row_ok (s : SessionListItemDict , parsed : SessionDict ) -> ProjectSessionRowDict :
15+ meta = parsed ["metadata" ]
16+ models = meta .get ("models_used" , [])
17+ return {
18+ "id" : s ["id" ],
19+ "path" : s ["path" ],
20+ "size_bytes" : s ["size_bytes" ],
21+ "modified" : s ["modified" ],
22+ "title" : parsed ["title" ],
23+ "models" : sorted (models ) if isinstance (models , set ) else list (models ),
24+ "tokens" : meta ["total_input_tokens" ] + meta ["total_output_tokens" ],
25+ "tool_calls" : meta ["total_tool_calls" ],
26+ "first_timestamp" : meta ["first_timestamp" ],
27+ "last_timestamp" : meta ["last_timestamp" ],
28+ }
29+
30+
31+ def _session_row_error (s : SessionListItemDict ) -> ProjectSessionRowDict :
32+ return {
33+ "id" : s ["id" ],
34+ "path" : s ["path" ],
35+ "size_bytes" : s ["size_bytes" ],
36+ "modified" : s ["modified" ],
37+ "title" : "Error parsing session" ,
38+ "error" : True ,
39+ }
40+
41+
1542@projects_bp .route ("/api/projects" )
1643def get_projects () -> FlaskReturn :
1744 base = current_app .config .get ("CLAUDE_PROJECTS_DIR" ) or get_claude_projects_dir ()
@@ -40,7 +67,7 @@ def get_projects() -> FlaskReturn:
4067 if latest_ts :
4168 project ["last_modified" ] = latest_ts
4269
43- return json_ok (projects )
70+ return json_response (projects )
4471
4572
4673@projects_bp .route ("/api/projects/<path:project_name>/sessions" )
@@ -49,7 +76,7 @@ def get_project_sessions(project_name: str) -> FlaskReturn:
4976 try :
5077 project_dir = safe_join (base , project_name )
5178 except ValueError :
52- return json_ok ([]), 400
79+ return json_response ([]), 400
5380 sessions = list_sessions (project_dir )
5481 # Add summary preview for each session
5582 from utils .jsonl_parser import parse_session
@@ -58,31 +85,17 @@ def get_project_sessions(project_name: str) -> FlaskReturn:
5885 for s in sessions :
5986 try :
6087 parsed = parse_session (s ["path" ])
61- meta = parsed ["metadata" ]
6288 # Skip untitled sessions (no real conversation)
6389 if parsed ["title" ] == "Untitled Session" :
6490 continue
6591 if is_session_excluded (rules , parsed , project_name ):
6692 continue
67- models = meta .get ("models_used" , [])
68- result .append (cast (ProjectSessionRowDict , {
69- ** s ,
70- "title" : parsed ["title" ],
71- "models" : sorted (models ) if isinstance (models , set ) else list (models ),
72- "tokens" : meta ["total_input_tokens" ] + meta ["total_output_tokens" ],
73- "tool_calls" : meta ["total_tool_calls" ],
74- "first_timestamp" : meta ["first_timestamp" ],
75- "last_timestamp" : meta ["last_timestamp" ],
76- }))
93+ result .append (_session_row_ok (s , parsed ))
7794 except Exception :
7895 # Full detail (class, message, traceback) to the server log via
7996 # logger.exception. The per-session card carries only `error: True`
8097 # — the class-name+message string was a leak (issue #25). The
8198 # operator looks at the server log for triage.
8299 current_app .logger .exception ("Failed to parse session %s" , s ["id" ])
83- result .append (cast (ProjectSessionRowDict , {
84- ** s ,
85- "title" : "Error parsing session" ,
86- "error" : True ,
87- }))
88- return json_ok (result )
100+ result .append (_session_row_error (s ))
101+ return json_response (result )
0 commit comments