4141
4242
4343async def _run_health_check () -> None :
44- """Check health of all deployed containers and record events."""
44+ """Check health of all deployed containers and record events.
45+
46+ Deduplicates by slug: if *any* instance of a service is running,
47+ record a single check_ok for that slug (avoids penalising services
48+ deployed on multiple nodes where one may be stopped).
49+ """
4550 try :
4651 statuses = orchestrator .get_status ()
52+ # Aggregate: slug -> best status (running wins)
53+ slug_best : dict [str , str ] = {}
4754 for s in statuses :
4855 slug = s ["slug" ]
4956 status = s .get ("status" , "unknown" )
57+ if slug_best .get (slug ) != "running" :
58+ slug_best [slug ] = status
59+ for slug , status in slug_best .items ():
5060 if status == "running" :
5161 await database .record_health_event (slug , "check_ok" )
5262 else :
@@ -431,13 +441,37 @@ async def api_list_services(request: Request) -> list[dict[str, Any]]:
431441
432442@app .get ("/api/services/deployed" )
433443async def api_services_deployed (request : Request ) -> list [dict [str , Any ]]:
434- """Return deployed services with container status, balance, CPU, memory."""
444+ """Return deployed services with container status, balance, CPU, memory.
445+
446+ Multiple containers for the same slug (multi-node) are aggregated into a
447+ single row with summed CPU/memory and an instance count.
448+ """
435449 _require_auth_api (request )
436450 try :
437451 statuses = orchestrator .get_status_cached ()
438452 except RuntimeError :
439453 statuses = []
440454
455+ # Also include worker containers
456+ workers = await database .list_workers ()
457+ for w in workers :
458+ if w .get ("status" ) != "online" :
459+ continue
460+ containers = json .loads (w .get ("containers" , "[]" ))
461+ for c in containers :
462+ slug = c .get ("slug" , "" )
463+ if slug :
464+ statuses .append ({
465+ "slug" : slug ,
466+ "name" : c .get ("name" , slug ),
467+ "status" : c .get ("status" , "unknown" ),
468+ "image" : c .get ("image" , "" ),
469+ "cpu_percent" : c .get ("cpu_percent" , 0 ),
470+ "memory_mb" : c .get ("memory_mb" , 0 ),
471+ "category" : "" ,
472+ "deployed_by" : w .get ("name" , "worker" ),
473+ })
474+
441475 # Get latest earnings per platform for balance display
442476 earnings = await database .get_earnings_summary ()
443477 balance_map = {e ["platform" ]: e ["balance" ] for e in earnings }
@@ -446,23 +480,44 @@ async def api_services_deployed(request: Request) -> list[dict[str, Any]]:
446480 health_scores = await database .get_health_scores (7 )
447481 health_map = {h ["slug" ]: h for h in health_scores }
448482
449- result = []
483+ # Aggregate by slug: one row per service
484+ _STATUS_PRIORITY = {"running" : 0 , "restarting" : 1 , "exited" : 2 , "created" : 3 , "dead" : 4 }
485+ slug_agg : dict [str , dict [str , Any ]] = {}
450486 for s in statuses :
451487 slug = s ["slug" ]
488+ if slug not in slug_agg :
489+ slug_agg [slug ] = {
490+ "instances" : [],
491+ "total_cpu" : 0.0 ,
492+ "total_mem" : 0.0 ,
493+ "best_status" : s .get ("status" , "unknown" ),
494+ "image" : s .get ("image" , "" ),
495+ }
496+ agg = slug_agg [slug ]
497+ agg ["instances" ].append (s )
498+ agg ["total_cpu" ] += float (s .get ("cpu_percent" , 0 ))
499+ agg ["total_mem" ] += float (s .get ("memory_mb" , 0 ))
500+ cur = s .get ("status" , "unknown" )
501+ if _STATUS_PRIORITY .get (cur , 9 ) < _STATUS_PRIORITY .get (agg ["best_status" ], 9 ):
502+ agg ["best_status" ] = cur
503+
504+ result = []
505+ for slug , agg in slug_agg .items ():
452506 svc = catalog .get_service (slug )
453507 health = health_map .get (slug , {})
454508 entry = {
455509 "slug" : slug ,
456510 "name" : svc ["name" ] if svc else slug ,
457- "container_status" : s [ "status " ],
511+ "container_status" : agg [ "best_status " ],
458512 "balance" : balance_map .get (slug , 0.0 ),
459- "cpu" : f"{ s . get ( 'cpu_percent' , 0 ) } " ,
460- "memory" : f"{ s . get ( 'memory_mb' , 0 ) } MB" ,
461- "image" : s . get ( "image" , "" ) ,
462- "category" : s .get ("category" , "" ),
513+ "cpu" : f"{ agg [ 'total_cpu' ]:.2f } " ,
514+ "memory" : f"{ agg [ 'total_mem' ]:.1f } MB" ,
515+ "image" : agg [ "image" ] ,
516+ "category" : agg [ "instances" ][ 0 ] .get ("category" , "" ),
463517 "health_score" : health .get ("score" ),
464518 "uptime_pct" : health .get ("uptime_pct" ),
465519 "restarts_7d" : health .get ("restarts" , 0 ),
520+ "instances" : len (agg ["instances" ]),
466521 }
467522 if svc :
468523 cashout = svc .get ("cashout" , {})
@@ -472,6 +527,41 @@ async def api_services_deployed(request: Request) -> list[dict[str, Any]]:
472527 if referral :
473528 entry ["referral_url" ] = referral .get ("signup_url" , "" )
474529 result .append (entry )
530+
531+ # Include external services (no Docker container, e.g. Grass, Bytelixir)
532+ seen_slugs = {r ["slug" ] for r in result }
533+ deployments = await database .get_deployments ()
534+ for d in deployments :
535+ slug = d ["slug" ]
536+ if slug in seen_slugs :
537+ continue
538+ if d .get ("status" ) != "external" :
539+ continue
540+ svc = catalog .get_service (slug )
541+ health = health_map .get (slug , {})
542+ entry = {
543+ "slug" : slug ,
544+ "name" : svc ["name" ] if svc else slug ,
545+ "container_status" : "external" ,
546+ "balance" : balance_map .get (slug , 0.0 ),
547+ "cpu" : "" ,
548+ "memory" : "" ,
549+ "image" : "" ,
550+ "category" : svc .get ("category" , "" ) if svc else "" ,
551+ "health_score" : health .get ("score" ),
552+ "uptime_pct" : health .get ("uptime_pct" ),
553+ "restarts_7d" : 0 ,
554+ "instances" : 0 ,
555+ }
556+ if svc :
557+ cashout = svc .get ("cashout" , {})
558+ if cashout :
559+ entry ["cashout" ] = cashout
560+ referral = svc .get ("referral" , {})
561+ if referral :
562+ entry ["referral_url" ] = referral .get ("signup_url" , "" )
563+ result .append (entry )
564+
475565 return result
476566
477567
0 commit comments