3636from boardwalkd .broadcast import handle_slack_broadcast
3737from boardwalkd .protocol import ApiLoginMessage , WorkspaceDetails , WorkspaceEvent
3838from boardwalkd .state import User , WorkspaceState , load_state , valid_user_roles
39+ from boardwalkd .utils import is_workspace_active
3940
4041module_dir = Path (__file__ ).resolve ().parent
4142state = load_state ()
@@ -518,10 +519,10 @@ def check_xsrf_cookie(self):
518519 pass
519520
520521 def get_current_user (self ) -> bytes | None :
521- """Decodes the API token to return the current logged in user"""
522+ """Decodes the API token to return the current logged in user. """
522523 return self .get_secure_cookie (
523524 "boardwalk_api_token" ,
524- value = self .request .headers [ "boardwalk-api-token" ] ,
525+ value = self .request .headers . get ( "boardwalk-api-token" ) ,
525526 max_age_days = self .settings ["auth_expire_days" ],
526527 min_version = 2 ,
527528 )
@@ -613,6 +614,32 @@ def get(self):
613614 return self .send_error (403 )
614615
615616
617+ class WorkspacesStatusApiHandler (APIBaseHandler ):
618+ """Returns an unauthenticated, read-only summary of all workspaces for monitoring integrations"""
619+
620+ # nosemgrep: test.boardwalk.python.security.handler-method-missing-authentication
621+ def get (self ):
622+ result = []
623+ for name , ws in state .workspaces .items ():
624+ entry : dict [str , Any ] = {
625+ "name" : name ,
626+ "semaphores" : ws .semaphores .model_dump (),
627+ }
628+ if ws .details :
629+ entry ["details" ] = {
630+ "workflow" : ws .details .workflow ,
631+ "worker" : f"{ ws .details .worker_username } @{ ws .details .worker_hostname } " ,
632+ "worker_connected" : is_workspace_active (workspace_name = name ),
633+ "host_pattern" : ws .details .host_pattern ,
634+ "limit_pattern" : "<unknown>" if ws .details .worker_limit == "" else ws .details .worker_limit ,
635+ "command" : ws .details .worker_command ,
636+ }
637+ if ws .last_seen :
638+ entry ["last_seen" ] = ws .last_seen .isoformat ()
639+ result .append (entry )
640+ self .write ({"workspaces" : result })
641+
642+
616643class WorkspaceCatchApiHandler (APIBaseHandler ):
617644 """Handles setting a catch on a workspace"""
618645
@@ -817,6 +844,7 @@ def make_app(
817844 slack_error_webhook_url : str ,
818845 slack_webhook_url : str ,
819846 url : str ,
847+ workspace_status_json : bool ,
820848) -> tornado .web .Application :
821849 """Builds the tornado application object"""
822850 handlers : list [tornado .web .OutputTransform ] = []
@@ -836,6 +864,7 @@ def make_app(
836864 "server_version" : ui_method_server_version ,
837865 "sort_events_by_date" : ui_method_sort_events_by_date ,
838866 },
867+ "workspace_status_json" : workspace_status_json ,
839868 "url" : urlparse (url ),
840869 "websocket_ping_interval" : 10 ,
841870 "xsrf_cookies" : True ,
@@ -901,6 +930,10 @@ def make_app(
901930 r"/api/auth/login/socket" ,
902931 AuthLoginApiWebsocketHandler ,
903932 ),
933+ (
934+ r"/api/workspaces/status" ,
935+ WorkspacesStatusApiHandler ,
936+ ),
904937 (
905938 r"/api/workspace/(\w+)/details" ,
906939 WorkspaceDetailsApiHandler ,
@@ -955,6 +988,7 @@ async def run(
955988 slack_webhook_url : str ,
956989 slack_slash_command_prefix : str ,
957990 url : str ,
991+ workspace_status_json : bool ,
958992):
959993 """Starts the tornado server and IO loop"""
960994 global state
@@ -969,6 +1003,7 @@ async def run(
9691003 slack_error_webhook_url = slack_error_webhook_url ,
9701004 slack_webhook_url = slack_webhook_url ,
9711005 url = url ,
1006+ workspace_status_json = workspace_status_json ,
9721007 )
9731008
9741009 if port_number is not None :
0 commit comments