@@ -138,6 +138,19 @@ def session_factory(engine: AsyncEngine) -> async_sessionmaker[AsyncSession]:
138138 return async_sessionmaker (engine , expire_on_commit = False )
139139
140140
141+ def _normalise_job_rows (rows : Iterable [dict ]) -> list [dict ]:
142+ normalised : list [dict ] = []
143+ for row in rows :
144+ payload = dict (row )
145+ payload ["repo_private" ] = True if payload .get ("repo_private" ) == "true" else False
146+ for key in ("queued_at" , "leased_at" , "completed_at" , "updated_at" ):
147+ value = payload .get (key )
148+ if isinstance (value , datetime ):
149+ payload [key ] = value .isoformat ()
150+ normalised .append (payload )
151+ return normalised
152+
153+
141154async def record_job_queued (session : AsyncSession , assignment : JobAssignment ) -> None :
142155 now = datetime .now (timezone .utc )
143156 repo = assignment .repository
@@ -209,30 +222,143 @@ async def record_status_update(session: AsyncSession, update_payload: JobStatusU
209222
210223
211224async def list_recent_jobs (
212- session : AsyncSession , limit : int = 50
225+ session : AsyncSession , limit : int = 50 , org_id : Optional [ int ] = None
213226) -> Iterable [dict ]:
214227 stmt = (
215228 select (jobs_table )
216229 .order_by (jobs_table .c .updated_at .desc ())
217230 .limit (limit )
218231 )
232+ if org_id is not None :
233+ stmt = stmt .where (jobs_table .c .org_id == org_id )
219234 result = await session .execute (stmt )
220235 rows = [dict (row ) for row in result .mappings ()]
221- for row in rows :
222- row ["repo_private" ] = True if row .get ("repo_private" ) == "true" else False
223- for key in ("queued_at" , "leased_at" , "completed_at" , "updated_at" ):
224- value = row .get (key )
225- if isinstance (value , datetime ):
226- row [key ] = value .isoformat ()
227- return rows
236+ return _normalise_job_rows (rows )
228237
229238
230- async def job_status_counts (session : AsyncSession ) -> dict [str , int ]:
239+ async def job_status_counts (
240+ session : AsyncSession ,
241+ * ,
242+ org_id : Optional [int ] = None ,
243+ ) -> dict [str , int ]:
231244 stmt = select (jobs_table .c .status , func .count ().label ("count" )).group_by (jobs_table .c .status )
245+ if org_id is not None :
246+ stmt = stmt .where (jobs_table .c .org_id == org_id )
232247 result = await session .execute (stmt )
233248 return {row .status : row .count for row in result }
234249
235250
251+ async def distinct_org_ids (
252+ session : AsyncSession ,
253+ * ,
254+ hours_back : Optional [int ] = None ,
255+ ) -> list [int ]:
256+ stmt = select (jobs_table .c .org_id ).distinct ().where (jobs_table .c .org_id .isnot (None ))
257+ if hours_back is not None :
258+ cutoff = datetime .now (timezone .utc ) - timedelta (hours = hours_back )
259+ stmt = stmt .where (jobs_table .c .updated_at >= cutoff )
260+ result = await session .execute (stmt )
261+ org_ids = [int (row .org_id ) for row in result if row .org_id is not None ]
262+ return sorted (set (org_ids ))
263+
264+
265+ async def org_job_status_counts (
266+ session : AsyncSession ,
267+ * ,
268+ org_id : Optional [int ] = None ,
269+ hours_back : Optional [int ] = None ,
270+ ) -> list [dict ]:
271+ stmt = select (
272+ jobs_table .c .org_id ,
273+ jobs_table .c .status ,
274+ func .count ().label ("count" ),
275+ )
276+ if org_id is not None :
277+ stmt = stmt .where (jobs_table .c .org_id == org_id )
278+ if hours_back is not None :
279+ cutoff = datetime .now (timezone .utc ) - timedelta (hours = hours_back )
280+ stmt = stmt .where (jobs_table .c .updated_at >= cutoff )
281+ stmt = stmt .group_by (jobs_table .c .org_id , jobs_table .c .status )
282+ result = await session .execute (stmt )
283+ return [
284+ {
285+ "org_id" : int (row .org_id ),
286+ "status" : row .status ,
287+ "count" : row .count ,
288+ }
289+ for row in result
290+ if row .org_id is not None
291+ ]
292+
293+
294+ async def org_last_activity (
295+ session : AsyncSession ,
296+ * ,
297+ org_id : Optional [int ] = None ,
298+ hours_back : Optional [int ] = None ,
299+ ) -> dict [int , datetime ]:
300+ stmt = select (
301+ jobs_table .c .org_id ,
302+ func .max (jobs_table .c .updated_at ).label ("last_updated" ),
303+ )
304+ if org_id is not None :
305+ stmt = stmt .where (jobs_table .c .org_id == org_id )
306+ if hours_back is not None :
307+ cutoff = datetime .now (timezone .utc ) - timedelta (hours = hours_back )
308+ stmt = stmt .where (jobs_table .c .updated_at >= cutoff )
309+ stmt = stmt .group_by (jobs_table .c .org_id )
310+ result = await session .execute (stmt )
311+ activity : dict [int , datetime ] = {}
312+ for row in result :
313+ if row .org_id is None :
314+ continue
315+ activity [int (row .org_id )] = row .last_updated
316+ return activity
317+
318+
319+ async def org_active_agents (
320+ session : AsyncSession ,
321+ * ,
322+ org_id : Optional [int ] = None ,
323+ hours_back : int = 24 ,
324+ ) -> dict [int , set [str ]]:
325+ cutoff = datetime .now (timezone .utc ) - timedelta (hours = hours_back )
326+ stmt = select (jobs_table .c .org_id , jobs_table .c .agent_id ).where (
327+ jobs_table .c .agent_id .isnot (None ),
328+ jobs_table .c .updated_at >= cutoff ,
329+ )
330+ if org_id is not None :
331+ stmt = stmt .where (jobs_table .c .org_id == org_id )
332+ result = await session .execute (stmt )
333+ mapping : dict [int , set [str ]] = {}
334+ for row in result :
335+ if row .org_id is None or not row .agent_id :
336+ continue
337+ bucket = mapping .setdefault (int (row .org_id ), set ())
338+ bucket .add (str (row .agent_id ))
339+ return mapping
340+
341+
342+ async def list_recent_failures (
343+ session : AsyncSession ,
344+ org_id : int ,
345+ * ,
346+ limit : int = 10 ,
347+ ) -> list [dict ]:
348+ stmt = (
349+ select (jobs_table )
350+ .where (
351+ jobs_table .c .org_id == org_id ,
352+ jobs_table .c .status .in_ (["failed" , "cancelled" ]),
353+ )
354+ .order_by (jobs_table .c .updated_at .desc ())
355+ .limit (limit )
356+ )
357+ result = await session .execute (stmt )
358+ rows = [dict (row ) for row in result .mappings ()]
359+ return _normalise_job_rows (rows )
360+
361+
236362async def get_job (session : AsyncSession , job_id : int ) -> Optional [dict ]:
237363 stmt = select (jobs_table ).where (jobs_table .c .job_id == job_id ).limit (1 )
238364 result = await session .execute (stmt )
0 commit comments