@@ -395,6 +395,7 @@ def _load_game_metadata(folder_info: dict[str, Any]) -> dict[str, Any]:
395395 folder_info ["models" ] = metadata .models
396396 folder_info ["game_name" ] = metadata .game_name
397397 folder_info ["created_timestamp" ] = metadata .get_path ("created_timestamp" )
398+ folder_info ["aws_command" ] = metadata .get_path ("aws.AWS_USER_PROVIDED_COMMAND" )
398399 return folder_info
399400
400401
@@ -409,20 +410,18 @@ def _find_all_game_folders_impl(base_dir: Path) -> list[dict[str, Any]]:
409410
410411 # Create folder info for each game folder
411412 game_folder_infos = []
412- game_folder_paths = set ()
413413
414414 for metadata_file in metadata_files :
415415 game_dir = metadata_file .parent
416416 relative_path = str (game_dir .relative_to (base_dir ))
417- game_folder_paths .add (relative_path )
418- depth = relative_path .count ("/" )
417+
418+ # Extract parent folder path (folder containing the game folder)
419+ parent_folder = str (game_dir .parent .relative_to (base_dir )) if game_dir .parent != base_dir else ""
419420
420421 folder_info = {
421422 "name" : relative_path ,
422423 "full_path" : str (game_dir ),
423- "is_game" : True ,
424- "depth" : depth ,
425- "parent" : str (game_dir .parent .relative_to (base_dir )) if game_dir .parent != base_dir else None ,
424+ "parent_folder" : parent_folder ,
426425 }
427426 game_folder_infos .append (folder_info )
428427
@@ -444,38 +443,7 @@ def _find_all_game_folders_impl(base_dir: Path) -> list[dict[str, Any]]:
444443 folder_info ["created_timestamp" ] = None
445444 logger .warning (f"Failed to load metadata for { folder_info ['name' ]} : { e } " )
446445
447- # Create intermediate folder entries for all parent directories
448- intermediate_folders = set ()
449- for game_path in game_folder_paths :
450- # Extract all parent paths
451- parts = game_path .split ("/" )
452- for i in range (1 , len (parts )):
453- parent_path = "/" .join (parts [:i ])
454- if parent_path not in game_folder_paths : # Only add if not a game folder itself
455- intermediate_folders .add (parent_path )
456-
457- # Add intermediate folders to the result
458- all_folders = game_folder_infos .copy ()
459- for intermediate_path in intermediate_folders :
460- depth = intermediate_path .count ("/" )
461- parent_parts = intermediate_path .split ("/" )
462- parent_path = "/" .join (parent_parts [:- 1 ]) if len (parent_parts ) > 1 else None
463-
464- all_folders .append (
465- {
466- "name" : intermediate_path ,
467- "full_path" : str (base_dir / intermediate_path ),
468- "is_game" : False ,
469- "depth" : depth ,
470- "parent" : parent_path ,
471- "round_info" : None ,
472- "models" : [],
473- "game_name" : "" ,
474- "created_timestamp" : None ,
475- }
476- )
477-
478- return sorted (all_folders , key = lambda x : x ["name" ])
446+ return sorted (game_folder_infos , key = lambda x : x ["name" ])
479447
480448
481449@print_timing
@@ -1012,14 +980,14 @@ def get_parent_folder(path):
1012980
1013981
1014982def format_timestamp (timestamp ):
1015- """Format Unix timestamp as YYYY-MM- DD HH:MM"""
983+ """Format Unix timestamp as MM/ DD HH:MM"""
1016984 if timestamp is None :
1017985 return ""
1018986 from datetime import datetime
1019987
1020988 try :
1021989 dt = datetime .fromtimestamp (timestamp )
1022- return dt .strftime ("%Y-%m- %d %H:%M" )
990+ return dt .strftime ("%m/ %d %H:%M" )
1023991 except (ValueError , OSError ):
1024992 return ""
1025993
@@ -1039,8 +1007,8 @@ def get_navigation_info(selected_folder: str) -> dict[str, str | None]:
10391007 # Get all game folders
10401008 game_folders = find_all_game_folders (LOG_BASE_DIR )
10411009
1042- # Filter to only actual game folders and sort them
1043- game_names = [folder ["name" ] for folder in game_folders if folder [ "is_game" ] ]
1010+ # Extract game folder names
1011+ game_names = [folder ["name" ] for folder in game_folders ]
10441012 game_names .sort ()
10451013
10461014 # Find current game index
@@ -1148,11 +1116,20 @@ def game_view(folder_path):
11481116@app .route ("/picker" )
11491117@print_timing
11501118def game_picker ():
1151- """Game picker page with recursive folder support """
1119+ """Game picker page with flat folder structure """
11521120 logs_dir = LOG_BASE_DIR
11531121 game_folders = find_all_game_folders (logs_dir )
11541122
1155- return render_template ("picker.html" , game_folders = game_folders , base_dir = str (logs_dir ), is_static = STATIC_MODE )
1123+ # Extract unique parent folders for the folder dropdown
1124+ unique_folders = sorted ({folder ["parent_folder" ] for folder in game_folders })
1125+
1126+ return render_template (
1127+ "picker.html" ,
1128+ game_folders = game_folders ,
1129+ unique_folders = unique_folders ,
1130+ base_dir = str (logs_dir ),
1131+ is_static = STATIC_MODE ,
1132+ )
11561133
11571134
11581135@app .route ("/delete-experiment" , methods = ["POST" ])
@@ -1191,198 +1168,6 @@ def delete_experiment():
11911168 return jsonify ({"success" : False , "error" : str (e )})
11921169
11931170
1194- @app .route ("/rename-folders" , methods = ["POST" ])
1195- def rename_folders ():
1196- """Add suffix to selected folders"""
1197- try :
1198- data = request .get_json ()
1199- action = data .get ("action" )
1200- paths = data .get ("paths" , [])
1201- suffix = data .get ("suffix" , "" )
1202-
1203- if not paths :
1204- return jsonify ({"success" : False , "error" : "No paths provided" })
1205-
1206- if action != "add-suffix" :
1207- return jsonify ({"success" : False , "error" : "Invalid action" })
1208-
1209- if not suffix :
1210- return jsonify ({"success" : False , "error" : "No suffix provided" })
1211-
1212- successful_renames = []
1213- failed_renames = []
1214-
1215- for relative_path in paths :
1216- try :
1217- # Get the full path
1218- old_path = LOG_BASE_DIR / relative_path
1219-
1220- if not old_path .exists ():
1221- failed_renames .append (f"{ relative_path } : does not exist" )
1222- continue
1223-
1224- # Create new path with suffix
1225- parent_dir = old_path .parent
1226- old_name = old_path .name
1227- new_name = f"{ old_name } .{ suffix } "
1228- new_path = parent_dir / new_name
1229-
1230- # Check if target already exists
1231- if new_path .exists ():
1232- failed_renames .append (f"{ relative_path } : target already exists" )
1233- continue
1234-
1235- # Perform the rename
1236- old_path .rename (new_path )
1237- successful_renames .append (f"{ relative_path } → { old_name } .{ suffix } " )
1238-
1239- except Exception as e :
1240- failed_renames .append (f"{ relative_path } : { str (e )} " )
1241-
1242- if failed_renames :
1243- error_msg = "Some renames failed:\n " + "\n " .join (failed_renames )
1244- if successful_renames :
1245- error_msg += "\n \n Successful renames:\n " + "\n " .join (successful_renames )
1246- return jsonify ({"success" : False , "error" : error_msg })
1247-
1248- return jsonify (
1249- {
1250- "success" : True ,
1251- "message" : f"Successfully renamed { len (successful_renames )} folder(s)" ,
1252- "renamed" : successful_renames ,
1253- }
1254- )
1255-
1256- except Exception as e :
1257- return jsonify ({"success" : False , "error" : str (e )})
1258-
1259-
1260- @app .route ("/move-to-subfolder" , methods = ["POST" ])
1261- def move_to_subfolder ():
1262- """Move selected folders to a new subfolder"""
1263- try :
1264- data = request .get_json ()
1265- paths = data .get ("paths" , [])
1266- subfolder_name = data .get ("subfolder" , "" )
1267-
1268- if not paths :
1269- return jsonify ({"success" : False , "error" : "No paths provided" })
1270-
1271- if not subfolder_name :
1272- return jsonify ({"success" : False , "error" : "No subfolder name provided" })
1273-
1274- # Validate subfolder name (no path separators, etc.)
1275- if "/" in subfolder_name or "\\ " in subfolder_name or ".." in subfolder_name :
1276- return jsonify ({"success" : False , "error" : "Invalid subfolder name" })
1277-
1278- successful_moves = []
1279- failed_moves = []
1280-
1281- for relative_path in paths :
1282- try :
1283- # Get the full path
1284- old_path = LOG_BASE_DIR / relative_path
1285-
1286- if not old_path .exists ():
1287- failed_moves .append (f"{ relative_path } : does not exist" )
1288- continue
1289-
1290- # Determine where to create the subfolder
1291- parent_dir = old_path .parent
1292- subfolder_path = parent_dir / subfolder_name
1293-
1294- # Create subfolder if it doesn't exist
1295- subfolder_path .mkdir (exist_ok = True )
1296-
1297- # Create new path inside subfolder
1298- folder_name = old_path .name
1299- new_path = subfolder_path / folder_name
1300-
1301- # Check if target already exists
1302- if new_path .exists ():
1303- failed_moves .append (f"{ relative_path } : target already exists in subfolder" )
1304- continue
1305-
1306- # Perform the move
1307- old_path .rename (new_path )
1308-
1309- # Calculate the new relative path for display
1310- new_relative_path = str (new_path .relative_to (LOG_BASE_DIR ))
1311- successful_moves .append (f"{ relative_path } → { new_relative_path } " )
1312-
1313- except Exception as e :
1314- failed_moves .append (f"{ relative_path } : { str (e )} " )
1315-
1316- if failed_moves :
1317- error_msg = "Some moves failed:\n " + "\n " .join (failed_moves )
1318- if successful_moves :
1319- error_msg += "\n \n Successful moves:\n " + "\n " .join (successful_moves )
1320- return jsonify ({"success" : False , "error" : error_msg })
1321-
1322- return jsonify (
1323- {
1324- "success" : True ,
1325- "message" : f"Successfully moved { len (successful_moves )} folder(s) to subfolder '{ subfolder_name } '" ,
1326- "moved" : successful_moves ,
1327- }
1328- )
1329-
1330- except Exception as e :
1331- return jsonify ({"success" : False , "error" : str (e )})
1332-
1333-
1334- @app .route ("/move-folder" , methods = ["POST" ])
1335- def move_folder ():
1336- """Move/rename a single folder to a new path"""
1337- try :
1338- data = request .get_json ()
1339- old_path = data .get ("old_path" , "" )
1340- new_path = data .get ("new_path" , "" )
1341-
1342- if not old_path :
1343- return jsonify ({"success" : False , "error" : "No old path provided" })
1344-
1345- if not new_path :
1346- return jsonify ({"success" : False , "error" : "No new path provided" })
1347-
1348- # Convert to Path objects relative to LOG_BASE_DIR
1349- old_full_path = LOG_BASE_DIR / old_path
1350- new_full_path = LOG_BASE_DIR / new_path
1351-
1352- # Validate old path exists
1353- if not old_full_path .exists ():
1354- return jsonify ({"success" : False , "error" : "Source folder does not exist" })
1355-
1356- # Validate new path doesn't already exist
1357- if new_full_path .exists ():
1358- return jsonify ({"success" : False , "error" : "Target path already exists" })
1359-
1360- # Security check: ensure both paths are within our expected logs directory
1361- try :
1362- old_full_path .relative_to (LOG_BASE_DIR )
1363- new_full_path .relative_to (LOG_BASE_DIR )
1364- except ValueError :
1365- return jsonify ({"success" : False , "error" : "Invalid path - must be within logs directory" })
1366-
1367- # Create intermediate directories if necessary
1368- new_full_path .parent .mkdir (parents = True , exist_ok = True )
1369-
1370- # Perform the move
1371- old_full_path .rename (new_full_path )
1372-
1373- return jsonify (
1374- {
1375- "success" : True ,
1376- "message" : f"Successfully moved folder from '{ old_path } ' to '{ new_path } '" ,
1377- "old_path" : old_path ,
1378- "new_path" : new_path ,
1379- }
1380- )
1381-
1382- except Exception as e :
1383- return jsonify ({"success" : False , "error" : str (e )})
1384-
1385-
13861171@timeout (120 )
13871172def _analyze_line_counts_impl (folder_path : Path ):
13881173 """Internal implementation of line counts analysis with timeout"""
0 commit comments