get('/edge-gateways', fn() => $this->handleListGateways(), [ 'modules_shelly_config' => 'Manage department edge gateways for local Shelly control', ]); $this->get('/edge-gateways/{id}', fn() => $this->handleGatewayDetail(), [ 'modules_shelly_config' => 'View department edge gateway detail', ]); $this->get('/edge-gateways/{id}/tasks', fn() => $this->handleGatewayTasksPage(), [ 'modules_shelly_config' => 'View edge gateway task timeline', ]); $this->get('/edge-gateways/{id}/logs', fn() => $this->handleGatewayLogsPage(), [ 'modules_shelly_config' => 'View edge gateway logs', ]); $this->get('/edge-gateways/{id}/statistics', fn() => $this->handleGatewayStatisticsPage(), [ 'modules_shelly_config' => 'View edge gateway statistics', ]); $this->post('/edge-gateways/{id}/stream-session', fn() => $this->handleGatewayStreamSessionCreate(), [ 'modules_shelly_config' => 'Create an edge gateway live stream session', ]); $this->post('/edge-gateways/{id}/shell-sessions', fn() => $this->handleGatewayShellSessionCreate(), [ 'modules_shelly_config' => 'Create an edge gateway shell session', ]); $this->put('/edge-gateways/{id}', fn() => $this->handleGatewayUpdate(), [ 'modules_shelly_config' => 'Update edge gateway metadata and primary assignment', ]); $this->get('/edge-gateways/{id}/operations', fn() => $this->handleGatewayOperationsList(), [ 'modules_shelly_config' => 'List edge gateway operations', ]); $this->post('/edge-gateways/{id}/operations', fn() => $this->handleGatewayOperationCreate(), [ 'modules_shelly_config' => 'Queue an edge gateway operation', ]); $this->post('/edge-gateways/{id}/operations/{operationId}/cancel', fn() => $this->handleGatewayOperationCancel(), [ 'modules_shelly_config' => 'Cancel an active edge gateway operation', ]); $this->get('/edge-gateways/{id}/operations/{operationId}/events', fn() => $this->handleGatewayOperationEvents(), [ 'modules_shelly_config' => 'List edge gateway operation events', ]); $this->post('/edge-gateways/{id}/rotate-credentials', fn() => $this->handleGatewayCredentialRotate(), [ 'modules_shelly_config' => 'Rotate edge gateway credentials', ]); $this->post('/edge-gateways/install-token', fn() => $this->handleInstallTokenCreate(), [ 'modules_shelly_config' => 'Create a one-time Raspberry Pi edge gateway installer token', ]); $this->get('/edge-gateways/install-token/{id}/status', fn() => $this->handleInstallTokenStatus(), [ 'modules_shelly_config' => 'View edge gateway installer session status', ]); $this->post('/edge-gateways/{id}/discovery', fn() => $this->handleGatewayDiscovery(), [ 'modules_shelly_config' => 'Queue Shelly discovery through the local edge gateway', ]); $this->put('/edge-gateways/{id}/bindings', fn() => $this->handleBindingsUpdate(), [ 'modules_shelly_config' => 'Approve or override relay bindings for an edge gateway', ]); $this->delete('/edge-gateways/{id}', fn() => $this->handleGatewayDelete(), [ 'modules_shelly_config' => 'Delete an edge gateway registration', ]); $this->post('/departments/{id}/gateway-cutover', fn() => $this->handleDepartmentCutover(), [ 'modules_shelly_config' => 'Cut a department over from Shelly cloud to local edge gateways', ]); $this->get('/edge-agent/install-token/verify', fn() => $this->handleInstallTokenVerify()); $this->post('/edge-agent/install-token/status', fn() => $this->handleAgentInstallTokenStatus()); $this->get('/edge-agent/install.sh', fn() => $this->renderInstallScript()); $this->get('/edge-agent/artifacts/manifest.json', fn() => $this->renderArtifact('manifest.json')); $this->get('/edge-agent/artifacts/agent.php', fn() => $this->renderArtifact('agent.php')); $this->get('/edge-agent/artifacts/lan-worker.php', fn() => $this->renderArtifact('lan-worker.php')); $this->get('/edge-agent/artifacts/auto-updater.php', fn() => $this->renderArtifact('auto-updater.php')); $this->get('/edge-agent/artifacts/docker-compose.gateway.yml', fn() => $this->renderArtifact('docker-compose.gateway.yml')); $this->get('/edge-agent/artifacts/Dockerfile.edge-agent', fn() => $this->renderArtifact('Dockerfile.edge-agent')); $this->get('/edge-agent/artifacts/Dockerfile.lan-worker', fn() => $this->renderArtifact('Dockerfile.lan-worker')); $this->get('/edge-agent/artifacts/Dockerfile.auto-updater', fn() => $this->renderArtifact('Dockerfile.auto-updater')); $this->get('/edge-agent/artifacts/gateway-launcher.sh', fn() => $this->renderArtifact('gateway-launcher.sh')); $this->get('/edge-agent/artifacts/truckwash-edge-gateway-stack.service', fn() => $this->renderArtifact('truckwash-edge-gateway-stack.service')); $this->get('/edge-agent/artifacts/truckwash-edge-agent.service', fn() => $this->renderArtifact('truckwash-edge-agent.service')); $this->post('/edge-agent/claim', fn() => $this->handleAgentClaim()); $this->post('/edge-agent/gateways/{id}/heartbeat', fn() => $this->handleAgentHeartbeat()); $this->post('/edge-agent/gateways/{id}/operations/next', fn() => $this->handleAgentOperationNext()); $this->post('/edge-agent/gateways/{id}/operations/{operationId}/events', fn() => $this->handleAgentOperationEvent()); $this->post('/edge-agent/gateways/{id}/operations/{operationId}/complete', fn() => $this->handleAgentOperationComplete()); $this->post('/edge-agent/gateways/{id}/commands/poll', fn() => $this->handleAgentCommandPoll()); $this->post('/edge-agent/gateways/{id}/commands/{jobId}/result', fn() => $this->handleAgentCommandResult()); $this->post('/edge-agent/gateways/{id}/expected-relay-states', fn() => $this->handleAgentExpectedRelayStates()); $this->post('/edge-agent/gateways/{id}/relay-state-results', fn() => $this->handleAgentRelayStateResults()); $this->post('/edge-agent/gateways/{id}/presence', fn() => $this->handleAgentPresence()); $this->post('/edge-agent/gateways/{id}/selfserve/machine-signal-bindings', fn() => $this->handleAgentSelfserveMachineSignalBindings()); $this->post('/edge-agent/gateways/{id}/selfserve/machine-signal', fn() => $this->handleAgentSelfserveMachineSignal()); $this->post('/edge-agent/internal/gateways/{id}/validate', fn() => $this->handleBrokerGatewayValidate()); $this->post('/edge-agent/internal/gateways/{id}/presence', fn() => $this->handleBrokerGatewayPresence()); $this->post('/edge-agent/internal/gateways/{id}/backlog', fn() => $this->handleBrokerGatewayBacklog()); $this->post('/edge-agent/internal/gateways/{id}/telemetry', fn() => $this->handleBrokerGatewayTelemetry()); $this->post('/edge-agent/internal/gateways/{id}/operations/{operationId}/events', fn() => $this->handleBrokerOperationEvent()); $this->post('/edge-agent/internal/gateways/{id}/operations/{operationId}/complete', fn() => $this->handleBrokerOperationComplete()); $this->post('/edge-agent/internal/gateways/{id}/logs', fn() => $this->handleBrokerGatewayLogEntry()); $this->post('/edge-agent/internal/gateways/{id}/selfserve/machine-signal', fn() => $this->handleBrokerSelfserveMachineSignal()); $this->post('/edge-agent/internal/browser-streams/validate', fn() => $this->handleBrokerBrowserStreamValidate()); $this->post('/edge-agent/internal/shell-sessions/validate', fn() => $this->handleBrokerShellSessionValidate()); $this->post('/edge-agent/internal/shell-sessions/opened', fn() => $this->handleBrokerShellSessionOpened()); $this->post('/edge-agent/internal/shell-sessions/close', fn() => $this->handleBrokerShellSessionClose()); } private function handleListGateways(): void { global /** @var response $response */ $response; $this->requirePermission('modules_shelly_config'); $departmentId = self::isParametersSet(['department_id']) ? (int)self::getParameter('department_id') : null; $view = trim((string)$this->fromQuery('view')); if ($departmentId !== null && $departmentId > 0) { $this->requireDepartmentAccess($departmentId); } $payload = $this->views()->listGatewaysWithFleetUsage($departmentId, $view !== 'summary'); $response->add_meta('fleet_usage', $payload['fleet_usage']); $response->success($payload['gateways']); } private function handleGatewayDetail(): void { global /** @var response $response */ $response; $this->requirePermission('modules_shelly_config'); $response->success($this->requireGatewayAccess((int)$this->fromRoute('id'))); } private function handleGatewayTasksPage(): void { global /** @var response $response */ $response; $this->requirePermission('modules_shelly_config'); $gatewayId = (int)$this->fromRoute('id'); $this->requireGatewayAccess($gatewayId); $response->success($this->manager()->buildGatewayTasksPage($gatewayId)); } private function handleGatewayLogsPage(): void { global /** @var response $response */ $response; $this->requirePermission('modules_shelly_config'); $gatewayId = (int)$this->fromRoute('id'); $this->requireGatewayAccess($gatewayId); $response->success($this->manager()->buildGatewayLogsPage($gatewayId)); } private function handleGatewayStatisticsPage(): void { global /** @var response $response */ $response; $this->requirePermission('modules_shelly_config'); $gatewayId = (int)$this->fromRoute('id'); $this->requireGatewayAccess($gatewayId); $response->success($this->manager()->buildGatewayStatisticsPage($gatewayId)); } private function handleGatewayStreamSessionCreate(): void { global /** @var response $response */ $response; $this->requirePermission('modules_shelly_config'); $gatewayId = (int)$this->fromRoute('id'); $this->requireGatewayAccess($gatewayId); $payload = self::getParametersAsArray(); $scopes = isset($payload['scopes']) && is_array($payload['scopes']) ? (array)$payload['scopes'] : []; $response->success($this->manager()->createBrowserStreamSession($gatewayId, $this->actorUserId(), $scopes), 201); } private function handleGatewayShellSessionCreate(): void { global /** @var response $response */ $response; $this->requirePermission('modules_shelly_config'); $gatewayId = (int)$this->fromRoute('id'); $this->requireGatewayAccess($gatewayId); $payload = self::getParametersAsArray(); $reason = isset($payload['reason']) ? (string)$payload['reason'] : ''; $cwd = isset($payload['cwd']) ? (string)$payload['cwd'] : null; $cols = isset($payload['cols']) ? (int)$payload['cols'] : null; $rows = isset($payload['rows']) ? (int)$payload['rows'] : null; try { $response->success($this->manager()->createShellSession($gatewayId, $this->actorUserId(), $reason, $cols, $rows, $cwd), 201); } catch (edge_gateway_operation_exception $exception) { $response->error([ 'message' => $exception->getMessage(), 'error_code' => $exception->errorCode, 'diagnostics' => $exception->details, ], $exception->status); } } private function handleGatewayUpdate(): void { global /** @var response $response */ $response; $this->requirePermission('modules_shelly_config'); self::requireParameters(['label', 'is_primary']); self::requireType(self::getParameter('label'), self::TYPE_STRING()); self::requireType(self::getParameter('is_primary'), self::TYPE_BOOL()); $gatewayId = (int)$this->fromRoute('id'); $this->requireGatewayAccess($gatewayId); $response->success($this->registry()->updateGatewayMetadata($gatewayId, [ 'label' => (string)self::getParameter('label'), 'is_primary' => (bool)self::getParameter('is_primary'), ], $this->actorUserId())); } private function handleGatewayOperationsList(): void { global /** @var response $response */ $response; $this->requirePermission('modules_shelly_config'); $gatewayId = (int)$this->fromRoute('id'); $this->requireGatewayAccess($gatewayId); $response->success($this->operations()->listOperations($gatewayId)); } private function handleGatewayOperationCreate(): void { global /** @var response $response */ $response; $this->requirePermission('modules_shelly_config'); self::requireParameters(['type', 'request']); self::requireType(self::getParameter('type'), self::TYPE_STRING()); self::requireType(self::getParameter('request'), self::TYPE_ARRAY()); $gatewayId = (int)$this->fromRoute('id'); $this->requireGatewayAccess($gatewayId); try { $operation = $this->operations()->queueOperation( $gatewayId, (string)self::getParameter('type'), (array)self::getParameter('request'), $this->actorUserId() ); $response->success([ 'operation' => $operation, 'gateway' => $this->views()->getGateway($gatewayId), ], 201); } catch (edge_gateway_operation_exception $exception) { $response->error([ 'message' => $exception->getMessage(), 'error_code' => $exception->errorCode, ], $exception->status); } } private function handleGatewayOperationEvents(): void { global /** @var response $response */ $response; $this->requirePermission('modules_shelly_config'); $gatewayId = (int)$this->fromRoute('id'); $operationId = (int)$this->fromRoute('operationId'); self::requireParameterIntPositive($operationId, 'operationId'); $this->requireGatewayAccess($gatewayId); $response->success($this->operations()->listOperationEvents($gatewayId, $operationId)); } private function handleGatewayOperationCancel(): void { global /** @var response $response */ $response; $this->requirePermission('modules_shelly_config'); $gatewayId = (int)$this->fromRoute('id'); $operationId = (int)$this->fromRoute('operationId'); self::requireParameterIntPositive($operationId, 'operationId'); $this->requireGatewayAccess($gatewayId); try { $operation = $this->operations()->cancelOperation($gatewayId, $operationId, $this->actorUserId()); $response->success([ 'operation' => $operation, 'gateway' => $this->views()->getGateway($gatewayId), ]); } catch (edge_gateway_operation_exception $exception) { $response->error([ 'message' => $exception->getMessage(), 'error_code' => $exception->errorCode, ], $exception->status); } } private function handleGatewayCredentialRotate(): void { global /** @var response $response */ $response; $this->requirePermission('modules_shelly_config'); $gatewayId = (int)$this->fromRoute('id'); $this->requireGatewayAccess($gatewayId); $response->success($this->operations()->rotateCredentials($gatewayId, $this->actorUserId())); } private function handleInstallTokenCreate(): void { global /** @var response $response */ $response; $this->requirePermission('modules_shelly_config'); self::requireParameters(['department_id']); $departmentId = (int)self::getParameter('department_id'); self::requireParameterIntPositive($departmentId, 'department_id'); $this->requireDepartmentAccess($departmentId); $response->success( $this->registry()->createInstallToken( $departmentId, self::isParametersSet(['label']) ? (string)self::getParameter('label') : null, $this->actorUserId() ), 201 ); } private function handleInstallTokenStatus(): void { global /** @var response $response */ $response; $this->requirePermission('modules_shelly_config'); $claimTokenId = (int)$this->fromRoute('id'); self::requireParameterIntPositive($claimTokenId, 'id'); $status = $this->registry()->getInstallTokenStatus($claimTokenId); $this->requireDepartmentAccess((int)$status['department_id']); unset($status['department_id']); $response->success($status); } private function handleGatewayDiscovery(): void { global /** @var response $response */ $response; $this->requirePermission('modules_shelly_config'); $gatewayId = (int)$this->fromRoute('id'); $this->requireGatewayAccess($gatewayId); $this->operations()->queueDiscoveryOperation($gatewayId, $this->actorUserId()); $response->success($this->views()->getGateway($gatewayId)); } private function handleBindingsUpdate(): void { global /** @var response $response */ $response; $this->requirePermission('modules_shelly_config'); self::requireParameters(['bindings']); self::requireType(self::getParameter('bindings'), self::TYPE_ARRAY()); $gatewayId = (int)$this->fromRoute('id'); $this->requireGatewayAccess($gatewayId); $this->registry()->setRelayBindings($gatewayId, (array)self::getParameter('bindings'), $this->actorUserId()); $response->success($this->views()->getGateway($gatewayId)); } private function handleGatewayDelete(): void { global /** @var response $response */ $response; $this->requirePermission('modules_shelly_config'); $gatewayId = (int)$this->fromRoute('id'); $this->requireGatewayAccess($gatewayId); $response->success($this->registry()->deleteGateway($gatewayId, $this->actorUserId())); } private function handleDepartmentCutover(): void { global /** @var response $response */ $response; $this->requirePermission('modules_shelly_config'); self::requireParameters(['transport_mode']); $departmentId = (int)$this->fromRoute('id'); self::requireParameterIntPositive($departmentId, 'id'); $this->requireDepartmentAccess($departmentId); $response->success($this->registry()->setDepartmentTransportMode($departmentId, (string)self::getParameter('transport_mode'), $this->actorUserId())); } private function renderInstallScript(): void { $token = trim((string)$this->fromQuery('token')); if ($token === '') { http_response_code(400); echo 'Missing token'; exit; } header('Content-Type: text/x-shellscript; charset=utf-8'); echo $this->install()->buildInstallScript($token); exit; } private function handleInstallTokenVerify(): void { global /** @var response $response */ $response; $token = trim((string)$this->fromQuery('token')); if ($token === '') { $response->error('Missing token', 400); } try { $response->success($this->install()->verifyInstallToken($token)); } catch (Exception $exception) { $response->error($exception->getMessage(), 400); } } private function handleAgentInstallTokenStatus(): void { global /** @var response $response */ $response; self::requireParameters(['token', 'status']); $payload = self::getParametersAsArray(); try { $response->success($this->registry()->reportInstallTokenStatus( (string)$payload['token'], [ 'status' => (string)$payload['status'], 'step' => isset($payload['step']) ? (string)$payload['step'] : null, 'message' => isset($payload['message']) ? (string)$payload['message'] : null, 'diagnostics' => isset($payload['diagnostics']) && is_array($payload['diagnostics']) ? (array)$payload['diagnostics'] : [], 'gateway_id' => isset($payload['gateway_id']) ? (int)$payload['gateway_id'] : null, 'last_error' => isset($payload['last_error']) ? (string)$payload['last_error'] : null, ] )); } catch (Exception $exception) { $response->error($exception->getMessage(), 400); } } private function renderArtifact(string $fileName): void { try { header('Content-Type: ' . $this->install()->contentType($fileName)); echo $this->install()->readArtifact($fileName); exit; } catch (Exception $exception) { http_response_code(404); echo $exception->getMessage(); exit; } } private function handleAgentClaim(): void { global /** @var response $response */ $response; self::requireParameters(['token']); $payload = self::getParametersAsArray(); try { $response->success( $this->registry()->claimGateway( (string)$payload['token'], trim((string)($payload['hostname'] ?? gethostname() ?: 'unknown-gateway')), isset($payload['installed_version']) ? (string)$payload['installed_version'] : null, isset($payload['metadata']) && is_array($payload['metadata']) ? (array)$payload['metadata'] : [] ), 201 ); } catch (Exception $exception) { $response->error($exception->getMessage(), 400); } } private function handleAgentHeartbeat(): void { global /** @var response $response */ $response; $gatewayId = (int)$this->fromRoute('id'); $payload = self::getParametersAsArray(); $response->success($this->registry()->recordHeartbeat($gatewayId, $this->requireAgentToken($payload), $payload)); } private function handleAgentOperationNext(): void { global /** @var response $response */ $response; $gatewayId = (int)$this->fromRoute('id'); $payload = self::getParametersAsArray(); try { $response->success($this->operations()->claimNextOperation( $gatewayId, $this->requireAgentToken($payload), isset($payload['wait_seconds']) ? (int)$payload['wait_seconds'] : edge_gateway_manager::COMMAND_POLL_TIMEOUT_SECONDS, isset($payload['agent_instance_id']) ? (string)$payload['agent_instance_id'] : null )); } catch (edge_gateway_operation_exception $exception) { $response->error([ 'message' => $exception->getMessage(), 'error_code' => $exception->errorCode, ], $exception->status); } } private function handleAgentOperationEvent(): void { global /** @var response $response */ $response; $gatewayId = (int)$this->fromRoute('id'); $operationId = (int)$this->fromRoute('operationId'); self::requireParameterIntPositive($operationId, 'operationId'); $payload = self::getParametersAsArray(); try { $response->success($this->operations()->appendAgentOperationEvent( $gatewayId, $operationId, $this->requireAgentToken($payload), $payload )); } catch (edge_gateway_operation_exception $exception) { $response->error([ 'message' => $exception->getMessage(), 'error_code' => $exception->errorCode, ], $exception->status); } } private function handleAgentOperationComplete(): void { global /** @var response $response */ $response; $gatewayId = (int)$this->fromRoute('id'); $operationId = (int)$this->fromRoute('operationId'); self::requireParameterIntPositive($operationId, 'operationId'); $payload = self::getParametersAsArray(); try { $response->success($this->operations()->completeAgentOperation( $gatewayId, $operationId, $this->requireAgentToken($payload), $payload )); } catch (edge_gateway_operation_exception $exception) { $response->error([ 'message' => $exception->getMessage(), 'error_code' => $exception->errorCode, ], $exception->status); } } private function handleAgentCommandPoll(): void { global /** @var response $response */ $response; $gatewayId = (int)$this->fromRoute('id'); $payload = self::getParametersAsArray(); $response->success($this->manager()->pollCommand( $gatewayId, $this->requireAgentToken($payload), isset($payload['wait_seconds']) ? (int)$payload['wait_seconds'] : edge_gateway_manager::COMMAND_POLL_TIMEOUT_SECONDS )); } private function handleAgentCommandResult(): void { global /** @var response $response */ $response; $gatewayId = (int)$this->fromRoute('id'); $jobId = (int)$this->fromRoute('jobId'); self::requireParameterIntPositive($jobId, 'jobId'); $payload = self::getParametersAsArray(); $response->success($this->manager()->submitCommandResult( $gatewayId, $jobId, $this->requireAgentToken($payload), (bool)($payload['ok'] ?? false), isset($payload['result']) && is_array($payload['result']) ? (array)$payload['result'] : [], isset($payload['error']) ? (string)$payload['error'] : null )); } private function handleAgentExpectedRelayStates(): void { global /** @var response $response */ $response; $gatewayId = (int)$this->fromRoute('id'); $payload = self::getParametersAsArray(); $response->success($this->manager()->buildExpectedRelayStatesForAgent( $gatewayId, $this->requireAgentToken($payload), isset($payload['wait_seconds']) ? (int)$payload['wait_seconds'] : 0 )); } private function handleAgentRelayStateResults(): void { global /** @var response $response */ $response; $gatewayId = (int)$this->fromRoute('id'); $payload = self::getParametersAsArray(); $results = isset($payload['results']) && is_array($payload['results']) ? (array)$payload['results'] : []; $response->success($this->manager()->recordExpectedRelayStateResults( $gatewayId, $this->requireAgentToken($payload), $results )); } private function handleAgentPresence(): void { global /** @var response $response */ $response; $gatewayId = (int)$this->fromRoute('id'); $payload = self::getParametersAsArray(); $this->requireAgentToken($payload); $response->success($this->manager()->recordBrokerPresence( $gatewayId, isset($payload['status']) ? (string)$payload['status'] : 'disconnected', isset($payload['connection_id']) ? (string)$payload['connection_id'] : null, isset($payload['reason']) ? (string)$payload['reason'] : null, isset($payload['metadata']) && is_array($payload['metadata']) ? (array)$payload['metadata'] : [] )); } private function handleAgentSelfserveMachineSignalBindings(): void { global /** @var response $response */ $response; $gatewayId = (int)$this->fromRoute('id'); $payload = self::getParametersAsArray(); try { $response->success((new selfserve_machine_signal())->listEdgeGatewayMachineSignalMonitors( $gatewayId, $this->requireAgentToken($payload) )); } catch (\Throwable $exception) { $response->error($exception->getMessage(), 400); } } private function handleAgentSelfserveMachineSignal(): void { global /** @var response $response */ $response; $gatewayId = (int)$this->fromRoute('id'); $payload = self::getParametersAsArray(); try { $result = (new selfserve_machine_signal())->recordEdgeGatewaySignal( $gatewayId, $this->requireAgentToken($payload), $payload ); $response->success($result, !empty($result['recorded']) ? 201 : 202); } catch (\Throwable $exception) { $response->error($exception->getMessage(), 400); } } private function handleBrokerGatewayValidate(): void { global /** @var response $response */ $response; $this->requireBrokerSecret(); self::requireParameters(['token']); $gatewayId = (int)$this->fromRoute('id'); $response->success($this->manager()->validateGatewayAgentForBroker($gatewayId, (string)self::getParameter('token'))); } private function handleBrokerGatewayPresence(): void { global /** @var response $response */ $response; $this->requireBrokerSecret(); $gatewayId = (int)$this->fromRoute('id'); $payload = self::getParametersAsArray(); $response->success($this->manager()->recordBrokerPresence( $gatewayId, isset($payload['status']) ? (string)$payload['status'] : 'disconnected', isset($payload['connection_id']) ? (string)$payload['connection_id'] : null, isset($payload['reason']) ? (string)$payload['reason'] : null, isset($payload['metadata']) && is_array($payload['metadata']) ? (array)$payload['metadata'] : [] )); } private function handleBrokerGatewayBacklog(): void { global /** @var response $response */ $response; $this->requireBrokerSecret(); $gatewayId = (int)$this->fromRoute('id'); $payload = self::getParametersAsArray(); $response->success($this->manager()->buildBrokerBacklog( $gatewayId, isset($payload['agent_instance_id']) ? (string)$payload['agent_instance_id'] : null )); } private function handleBrokerGatewayTelemetry(): void { global /** @var response $response */ $response; $this->requireBrokerSecret(); $gatewayId = (int)$this->fromRoute('id'); $payload = self::getParametersAsArray(); $response->success($this->manager()->recordTelemetryFromBroker($gatewayId, $payload)); } private function handleBrokerOperationEvent(): void { global /** @var response $response */ $response; $this->requireBrokerSecret(); $gatewayId = (int)$this->fromRoute('id'); $operationId = (int)$this->fromRoute('operationId'); self::requireParameterIntPositive($operationId, 'operationId'); $payload = self::getParametersAsArray(); try { $response->success($this->operations()->appendBrokerOperationEvent($gatewayId, $operationId, $payload)); } catch (edge_gateway_operation_exception $exception) { $response->error([ 'message' => $exception->getMessage(), 'error_code' => $exception->errorCode, ], $exception->status); } } private function handleBrokerOperationComplete(): void { global /** @var response $response */ $response; $this->requireBrokerSecret(); $gatewayId = (int)$this->fromRoute('id'); $operationId = (int)$this->fromRoute('operationId'); self::requireParameterIntPositive($operationId, 'operationId'); $payload = self::getParametersAsArray(); try { $response->success($this->operations()->completeBrokerOperation($gatewayId, $operationId, $payload)); } catch (edge_gateway_operation_exception $exception) { $response->error([ 'message' => $exception->getMessage(), 'error_code' => $exception->errorCode, ], $exception->status); } } private function handleBrokerGatewayLogEntry(): void { global /** @var response $response */ $response; $this->requireBrokerSecret(); self::requireParameters(['message']); $gatewayId = (int)$this->fromRoute('id'); $payload = self::getParametersAsArray(); $response->success($this->manager()->appendGatewayLogEntry( $gatewayId, (string)self::getParameter('message'), isset($payload['level']) ? (string)$payload['level'] : 'INFO', isset($payload['stream']) ? (string)$payload['stream'] : 'agent', isset($payload['source']) ? (string)$payload['source'] : 'BROKER', isset($payload['context']) && is_array($payload['context']) ? (array)$payload['context'] : [] )); } private function handleBrokerSelfserveMachineSignal(): void { global /** @var response $response */ $response; $this->requireBrokerSecret(); $gatewayId = (int)$this->fromRoute('id'); $payload = self::getParametersAsArray(); try { $result = (new selfserve_machine_signal())->recordBrokerEdgeGatewaySignal($gatewayId, $payload); $response->success($result, !empty($result['recorded']) ? 201 : 202); } catch (\Throwable $exception) { $response->error($exception->getMessage(), 400); } } private function handleBrokerBrowserStreamValidate(): void { global /** @var response $response */ $response; $this->requireBrokerSecret(); self::requireParameters(['token']); $response->success($this->manager()->validateBrowserStreamToken((string)self::getParameter('token'))); } private function handleBrokerShellSessionValidate(): void { global /** @var response $response */ $response; $this->requireBrokerSecret(); self::requireParameters(['token']); $response->success($this->manager()->validateShellSessionToken((string)self::getParameter('token'))); } private function handleBrokerShellSessionOpened(): void { global /** @var response $response */ $response; $this->requireBrokerSecret(); self::requireParameters(['token']); $payload = self::getParametersAsArray(); $response->success($this->manager()->markShellSessionOpened( (string)self::getParameter('token'), isset($payload['connection_id']) ? (string)$payload['connection_id'] : null )); } private function handleBrokerShellSessionClose(): void { global /** @var response $response */ $response; $this->requireBrokerSecret(); self::requireParameters(['token']); $payload = self::getParametersAsArray(); $response->success($this->manager()->closeShellSessionByToken( (string)self::getParameter('token'), isset($payload['transcript']) ? (string)$payload['transcript'] : '', isset($payload['reason']) ? (string)$payload['reason'] : null, [ 'message' => isset($payload['message']) ? (string)$payload['message'] : null, 'code' => isset($payload['code']) ? (int)$payload['code'] : null, 'close_code' => isset($payload['close_code']) ? (int)$payload['close_code'] : null, 'stage' => isset($payload['stage']) ? (string)$payload['stage'] : null, 'failure_stage' => isset($payload['failure_stage']) ? (string)$payload['failure_stage'] : null, 'was_clean' => isset($payload['was_clean']) ? (bool)$payload['was_clean'] : null, 'connection_id' => isset($payload['connection_id']) ? (string)$payload['connection_id'] : null, 'broker_connection_id' => isset($payload['broker_connection_id']) ? (string)$payload['broker_connection_id'] : null, 'broker_url' => isset($payload['broker_url']) ? (string)$payload['broker_url'] : null, 'ws_url' => isset($payload['ws_url']) ? (string)$payload['ws_url'] : null, 'details' => isset($payload['details']) && is_array($payload['details']) ? (array)$payload['details'] : [], ] )); } private function requireGatewayAccess(int $gatewayId): array { self::requireParameterIntPositive($gatewayId, 'id'); $gateway = $this->views()->getGateway($gatewayId); $this->requireDepartmentAccess((int)$gateway['department_id']); return $gateway; } private function requireAgentToken(array $payload): string { global /** @var response $response */ $response; $token = trim((string)($payload['agent_token'] ?? $this->fromRequest('agent_token'))); if ($token === '') { $response->error('Missing edge gateway agent token', 401); } return $token; } private function requireBrokerSecret(): void { global /** @var response $response */ $response; $provided = trim((string)($_SERVER['HTTP_X_EDGE_BROKER_SECRET'] ?? '')); if (!$this->manager()->validateBrokerSharedSecret($provided)) { $response->error('Invalid edge broker secret', 403); } } private function actorUserId(): ?int { $user = (new authentication())->get_user(); return $user ? (int)$user->id : null; } private function views(): edge_gateway_view_service { return new edge_gateway_view_service(); } private function registry(): edge_gateway_registry_service { return new edge_gateway_registry_service(); } private function manager(): edge_gateway_manager { return new edge_gateway_manager(); } private function operations(): edge_gateway_operation_service { return new edge_gateway_operation_service($this->manager()); } private function install(): edge_gateway_install_service { return new edge_gateway_install_service(); } }