diff --git a/src/main/java/net/siegeln/cameleer/saas/identity/ServerApiClient.java b/src/main/java/net/siegeln/cameleer/saas/identity/ServerApiClient.java index f8db0fc..370fbc6 100644 --- a/src/main/java/net/siegeln/cameleer/saas/identity/ServerApiClient.java +++ b/src/main/java/net/siegeln/cameleer/saas/identity/ServerApiClient.java @@ -157,6 +157,22 @@ public class ServerApiClient { } } + /** Fetch app count from a tenant's server. */ + public int getAppCount(String serverEndpoint) { + try { + var resp = RestClient.create().get() + .uri(serverEndpoint + "/api/v1/admin/apps") + .header("Authorization", "Bearer " + getAccessToken()) + .header("X-Cameleer-Protocol-Version", "1") + .retrieve() + .body(java.util.List.class); + return resp != null ? resp.size() : 0; + } catch (Exception e) { + log.warn("App count fetch failed for {}: {}", serverEndpoint, e.getMessage()); + return 0; + } + } + /** Reset the built-in admin password on a tenant's server. */ public void resetServerAdminPassword(String serverEndpoint, String newPassword) { RestClient.create(serverEndpoint) diff --git a/src/main/java/net/siegeln/cameleer/saas/portal/TenantPortalService.java b/src/main/java/net/siegeln/cameleer/saas/portal/TenantPortalService.java index fa90961..8cc6794 100644 --- a/src/main/java/net/siegeln/cameleer/saas/portal/TenantPortalService.java +++ b/src/main/java/net/siegeln/cameleer/saas/portal/TenantPortalService.java @@ -68,7 +68,8 @@ public class TenantPortalService { public record LicenseData( UUID id, String tier, String label, Map limits, int gracePeriodDays, Instant issuedAt, Instant expiresAt, - String token, long daysRemaining + String token, long daysRemaining, + Map usage ) {} public record TenantSettingsData( @@ -140,16 +141,36 @@ public class TenantPortalService { public LicenseData getLicense() { TenantEntity tenant = resolveTenant(); return licenseService.getActiveLicense(tenant.getId()) - .map(lic -> new LicenseData( - lic.getId(), lic.getTier(), lic.getLabel(), - lic.getLimits() != null ? lic.getLimits() : Map.of(), - lic.getGracePeriodDays(), - lic.getIssuedAt(), lic.getExpiresAt(), - lic.getToken(), daysUntil(lic.getExpiresAt()) - )) + .map(lic -> { + Map usage = fetchUsage(tenant); + return new LicenseData( + lic.getId(), lic.getTier(), lic.getLabel(), + lic.getLimits() != null ? lic.getLimits() : Map.of(), + lic.getGracePeriodDays(), + lic.getIssuedAt(), lic.getExpiresAt(), + lic.getToken(), daysUntil(lic.getExpiresAt()), + usage + ); + }) .orElse(null); } + private Map fetchUsage(TenantEntity tenant) { + Map usage = new HashMap<>(); + String endpoint = tenant.getServerEndpoint(); + if (endpoint != null && !endpoint.isBlank()) { + usage.put("agents", serverApiClient.getAgentCount(endpoint)); + usage.put("environments", serverApiClient.getEnvironmentCount(endpoint)); + usage.put("apps", serverApiClient.getAppCount(endpoint)); + } + // User count from Logto org membership + String orgId = tenant.getLogtoOrgId(); + if (orgId != null && !orgId.isBlank()) { + usage.put("users", logtoClient.listOrganizationMembers(orgId).size()); + } + return usage; + } + public List> listTeamMembers() { TenantEntity tenant = resolveTenant(); String orgId = tenant.getLogtoOrgId(); diff --git a/src/main/java/net/siegeln/cameleer/saas/vendor/VendorTenantController.java b/src/main/java/net/siegeln/cameleer/saas/vendor/VendorTenantController.java index fda8f3e..46d7e12 100644 --- a/src/main/java/net/siegeln/cameleer/saas/vendor/VendorTenantController.java +++ b/src/main/java/net/siegeln/cameleer/saas/vendor/VendorTenantController.java @@ -23,6 +23,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.List; +import java.util.Map; import java.util.UUID; @RestController @@ -57,7 +58,8 @@ public class VendorTenantController { String serverState, boolean serverHealthy, String serverStatus, - LicenseResponse license + LicenseResponse license, + Map usage ) {} // --- Endpoints --- @@ -123,12 +125,23 @@ public class VendorTenantController { .getLicenseForTenant(id) .map(LicenseResponse::from) .orElse(null); + + Map usage = new java.util.HashMap<>(); + String endpoint = tenant.getServerEndpoint(); + if (health.healthy() && endpoint != null && !endpoint.isBlank()) { + var serverApi = vendorTenantService.getServerApiClient(); + usage.put("agents", serverApi.getAgentCount(endpoint)); + usage.put("environments", serverApi.getEnvironmentCount(endpoint)); + usage.put("apps", serverApi.getAppCount(endpoint)); + } + return ResponseEntity.ok(new VendorTenantDetail( TenantResponse.from(tenant), serverStatus.state().name(), health.healthy(), health.status(), - license + license, + usage )); }) .orElse(ResponseEntity.notFound().build());