@@ -2006,6 +2006,83 @@ def _get_database_size_info(self) -> dict[str, Any]:
20062006 info ["error" ] = str (e )
20072007 return info
20082008
2009+ def _get_purge_activity_info (self ) -> dict [str , Any ]:
2010+ """Compute how much space each retention level would free by NULLing run_track_activity_jsonl."""
2011+ from sqlalchemy import text
2012+ info : dict [str , Any ] = {"error" : None , "total_rows" : 0 , "rows_with_data" : 0 , "total_data_mb" : 0.0 , "options" : []}
2013+ try :
2014+ with self .db .engine .connect () as conn :
2015+ row = conn .execute (text (
2016+ "SELECT count(*), "
2017+ "count(run_track_activity_jsonl), "
2018+ "coalesce(sum(octet_length(run_track_activity_jsonl)), 0) "
2019+ "FROM task_item"
2020+ )).fetchone ()
2021+ if row :
2022+ info ["total_rows" ] = row [0 ]
2023+ info ["rows_with_data" ] = row [1 ]
2024+ info ["total_data_mb" ] = round (row [2 ] / (1024 * 1024 ), 2 )
2025+
2026+ for keep_n in [10 , 25 , 50 , 100 , 250 , 500 ]:
2027+ result = conn .execute (text (
2028+ "SELECT coalesce(sum(octet_length(run_track_activity_jsonl)), 0), count(*) "
2029+ "FROM task_item "
2030+ "WHERE run_track_activity_jsonl IS NOT NULL "
2031+ "AND id NOT IN ("
2032+ " SELECT id FROM task_item "
2033+ " ORDER BY timestamp_created DESC "
2034+ " LIMIT :keep_n"
2035+ ")"
2036+ ), {"keep_n" : keep_n }).fetchone ()
2037+ if result :
2038+ info ["options" ].append ({
2039+ "keep_n" : keep_n ,
2040+ "purgeable_rows" : result [1 ],
2041+ "savings_bytes" : result [0 ],
2042+ "savings_mb" : round (result [0 ] / (1024 * 1024 ), 2 ),
2043+ })
2044+ except Exception as e :
2045+ logger .exception ("Failed to query purge activity info" )
2046+ info ["error" ] = str (e )
2047+ return info
2048+
2049+ def _purge_activity_data (self , keep_n : int ) -> dict [str , Any ]:
2050+ """NULL out run_track_activity_jsonl for all rows except the latest keep_n."""
2051+ from sqlalchemy import text
2052+ result : dict [str , Any ] = {"error" : None , "purged_rows" : 0 }
2053+ try :
2054+ with self .db .engine .connect () as conn :
2055+ row = conn .execute (text (
2056+ "UPDATE task_item "
2057+ "SET run_track_activity_jsonl = NULL, run_track_activity_bytes = NULL "
2058+ "WHERE run_track_activity_jsonl IS NOT NULL "
2059+ "AND id NOT IN ("
2060+ " SELECT id FROM task_item "
2061+ " ORDER BY timestamp_created DESC "
2062+ " LIMIT :keep_n"
2063+ ")"
2064+ ), {"keep_n" : keep_n })
2065+ result ["purged_rows" ] = row .rowcount
2066+ conn .commit ()
2067+ except Exception as e :
2068+ logger .exception ("Failed to purge activity data" )
2069+ result ["error" ] = str (e )
2070+ return result
2071+
2072+ def _vacuum_task_item (self ) -> dict [str , Any ]:
2073+ """Run VACUUM FULL on task_item to reclaim disk space."""
2074+ from sqlalchemy import text
2075+ result : dict [str , Any ] = {"error" : None }
2076+ try :
2077+ with self .db .engine .connect () as conn :
2078+ conn .execution_options (isolation_level = "AUTOCOMMIT" ).execute (
2079+ text ("VACUUM FULL task_item" )
2080+ )
2081+ except Exception as e :
2082+ logger .exception ("Failed to vacuum task_item" )
2083+ result ["error" ] = str (e )
2084+ return result
2085+
20092086 def _build_reconciliation_report (self , max_tasks : int , tolerance_usd : float ) -> tuple [list [dict [str , Any ]], dict [str , Any ]]:
20102087 tasks = (
20112088 PlanItem .query
@@ -2977,13 +3054,28 @@ def admin_reconciliation():
29773054 refresh_seconds = refresh_seconds ,
29783055 )
29793056
2980- @self .app .route ('/admin/db-size' )
3057+ @self .app .route ('/admin/database' , methods = [ 'GET' , 'POST' ] )
29813058 @admin_required
2982- def admin_db_size ():
3059+ def admin_database ():
3060+ purge_result = None
3061+ vacuum_result = None
3062+ if request .method == 'POST' :
3063+ action = request .form .get ('action' , '' )
3064+ if action == 'purge' :
3065+ keep_n = int (request .form .get ('keep_n' , '50' ) or '50' )
3066+ if keep_n not in (10 , 25 , 50 , 100 , 250 , 500 ):
3067+ keep_n = 50
3068+ purge_result = self ._purge_activity_data (keep_n )
3069+ elif action == 'vacuum' :
3070+ vacuum_result = self ._vacuum_task_item ()
29833071 size_info = self ._get_database_size_info ()
3072+ purge_info = self ._get_purge_activity_info ()
29843073 return self .admin .index_view .render (
2985- "admin/db_size .html" ,
3074+ "admin/database .html" ,
29863075 size_info = size_info ,
3076+ purge_info = purge_info ,
3077+ purge_result = purge_result ,
3078+ vacuum_result = vacuum_result ,
29873079 )
29883080
29893081 @self .app .route ('/ping/stream' )
0 commit comments