1010from pathlib import Path
1111from typing import Any
1212
13- from flask import Flask , jsonify , render_template , request
13+ from flask import Flask , jsonify , redirect , render_template , request , url_for
1414
1515# Global variable to store the directory to search for logs
1616LOG_BASE_DIR = Path .cwd () / "logs"
@@ -22,10 +22,10 @@ def set_log_base_directory(directory: str | Path):
2222 LOG_BASE_DIR = Path (directory ).resolve ()
2323
2424
25- def is_probably_failed_run (log_dir : Path ) -> bool :
26- """Check if a run probably failed by checking if metadata.json is missing """
25+ def is_game_folder (log_dir : Path ) -> bool :
26+ """Check if a directory contains metadata.json and is therefore a game folder """
2727 metadata_file = log_dir / "metadata.json"
28- return not metadata_file .exists ()
28+ return metadata_file .exists ()
2929
3030
3131def get_round_count_from_metadata (log_dir : Path ) -> int | None :
@@ -41,6 +41,73 @@ def get_round_count_from_metadata(log_dir: Path) -> int | None:
4141 return None
4242
4343
44+ def find_all_game_folders (base_dir : Path ) -> list [dict [str , Any ]]:
45+ """Recursively find all folders and mark which ones contain metadata.json"""
46+ all_folders = []
47+ game_folders = set () # Track which folders are actual game folders
48+
49+ def scan_directory (directory : Path , relative_path : str = "" ):
50+ if not directory .exists () or not directory .is_dir ():
51+ return
52+
53+ try :
54+ for item in directory .iterdir ():
55+ if item .is_dir ():
56+ current_relative = relative_path + "/" + item .name if relative_path else item .name
57+
58+ depth = current_relative .count ("/" )
59+
60+ # Check if this directory is a game folder
61+ if is_game_folder (item ):
62+ round_count = get_round_count_from_metadata (item )
63+ game_folders .add (current_relative )
64+ all_folders .append (
65+ {
66+ "name" : current_relative ,
67+ "full_path" : str (item ),
68+ "round_count" : round_count ,
69+ "is_game" : True ,
70+ "depth" : depth ,
71+ "parent" : relative_path if relative_path else None ,
72+ }
73+ )
74+ else :
75+ # Add as intermediate folder if it has game folders in subdirectories
76+ all_folders .append (
77+ {
78+ "name" : current_relative ,
79+ "full_path" : str (item ),
80+ "round_count" : None ,
81+ "is_game" : False ,
82+ "depth" : depth ,
83+ "parent" : relative_path if relative_path else None ,
84+ }
85+ )
86+
87+ # Recursively scan subdirectories
88+ scan_directory (item , current_relative )
89+ except (PermissionError , OSError ):
90+ # Skip directories we can't access
91+ pass
92+
93+ scan_directory (base_dir )
94+
95+ # Filter out intermediate folders that don't lead to any game folders
96+ filtered_folders = []
97+ for folder in sorted (all_folders , key = lambda x : x ["name" ]):
98+ if folder ["is_game" ]:
99+ # Always include game folders
100+ filtered_folders .append (folder )
101+ else :
102+ # Include intermediate folders only if they have game folders as descendants
103+ folder_path = folder ["name" ]
104+ has_game_descendants = any (game_path .startswith (folder_path + "/" ) for game_path in game_folders )
105+ if has_game_descendants :
106+ filtered_folders .append (folder )
107+
108+ return filtered_folders
109+
110+
44111@dataclass
45112class GameMetadata :
46113 """Metadata about a game session"""
@@ -245,33 +312,21 @@ def unescape_content(value):
245312
246313@app .route ("/" )
247314def index ():
248- """Main viewer page"""
249- # Get available log directories
250- logs_dir = LOG_BASE_DIR
251- log_folders_info = []
252- if logs_dir .exists ():
253- for d in logs_dir .iterdir ():
254- if d .is_dir ():
255- folder_info = {
256- "name" : d .name ,
257- "is_failed" : is_probably_failed_run (d ),
258- "round_count" : get_round_count_from_metadata (d ),
259- }
260- log_folders_info .append (folder_info )
261-
262- # Sort folders alphabetically by name
263- log_folders_info .sort (key = lambda x : x ["name" ])
315+ """Main viewer page - now redirects to picker if no folder is selected"""
316+ selected_folder = request .args .get ("folder" )
264317
265- # Extract just the names for backwards compatibility
266- log_folders = [ folder [ "name" ] for folder in log_folders_info ]
318+ if not selected_folder :
319+ return redirect ( url_for ( "game_picker" ))
267320
268- selected_folder = request .args .get ("folder" , log_folders [0 ] if log_folders else None )
321+ # Validate the selected folder exists and is a game folder
322+ logs_dir = LOG_BASE_DIR
323+ folder_path = logs_dir / selected_folder
269324
270- if not selected_folder or not ( logs_dir / selected_folder ). exists ( ):
271- return render_template ( "no_logs.html" , log_folders = log_folders )
325+ if not folder_path . exists () or not is_game_folder ( folder_path ):
326+ return redirect ( url_for ( "game_picker" ) )
272327
273328 # Parse the selected game
274- parser = LogParser (logs_dir / selected_folder )
329+ parser = LogParser (folder_path )
275330 metadata = parser .parse_game_metadata ()
276331 available_trajectories = parser .get_available_trajectories ()
277332
@@ -285,19 +340,26 @@ def index():
285340 trajectories_by_round [round_num ].append (trajectory )
286341
287342 # Get the full path of the selected folder
288- selected_folder_path = str (logs_dir / selected_folder ) if selected_folder else ""
343+ selected_folder_path = str (folder_path )
289344
290345 return render_template (
291346 "index.html" ,
292- log_folders = log_folders ,
293- log_folders_info = log_folders_info ,
294347 selected_folder = selected_folder ,
295348 selected_folder_path = selected_folder_path ,
296349 metadata = metadata ,
297350 trajectories_by_round = trajectories_by_round ,
298351 )
299352
300353
354+ @app .route ("/picker" )
355+ def game_picker ():
356+ """Game picker page with recursive folder support"""
357+ logs_dir = LOG_BASE_DIR
358+ game_folders = find_all_game_folders (logs_dir )
359+
360+ return render_template ("picker.html" , game_folders = game_folders , base_dir = str (logs_dir ))
361+
362+
301363@app .route ("/trajectory/<int:player_id>/<int:round_num>" )
302364def trajectory_detail (player_id : int , round_num : int ):
303365 """Get detailed trajectory data"""
0 commit comments