From 6afc337b167ff7cbbfb2d86c3aa7a39453564e4f Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Sun, 26 Apr 2026 22:08:47 +0200 Subject: [PATCH] feat: add usage data to license and vendor detail endpoints Add getAppCount() to ServerApiClient, include usage counts (agents, environments, apps, users) in tenant license and vendor detail responses so the frontend can render progress bars against license limits. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../saas/identity/ServerApiClient.java | 16 ++++++++ .../saas/portal/TenantPortalService.java | 37 +++++++++++++++---- .../saas/vendor/VendorTenantController.java | 17 ++++++++- 3 files changed, 60 insertions(+), 10 deletions(-) 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());