# Fleet Health at a Glance Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Add agent count, environment count, and agent limit columns to the vendor tenant list so the vendor can see fleet utilization at a glance. **Architecture:** Extend the existing `VendorTenantSummary` record with three int fields. The list endpoint fetches counts from each active tenant's server via existing M2M API methods (`getAgentCount`, `getEnvironmentCount`), parallelized with `CompletableFuture`. Frontend adds two columns (Agents, Envs) to the DataTable. **Tech Stack:** Java 21, Spring Boot, CompletableFuture, React, TypeScript, @cameleer/design-system DataTable --- ### Task 1: Extend backend — VendorTenantSummary + parallel fetch **Files:** - Modify: `src/main/java/net/siegeln/cameleer/saas/vendor/VendorTenantController.java` - [ ] **Step 1: Extend the VendorTenantSummary record** In `VendorTenantController.java`, replace the record at lines 39-48: ```java public record VendorTenantSummary( UUID id, String name, String slug, String tier, String status, String serverState, String licenseExpiry, String provisionError, int agentCount, int environmentCount, int agentLimit ) {} ``` - [ ] **Step 2: Update the listAll() endpoint to fetch counts in parallel** Replace the `listAll()` method at lines 60-77: ```java @GetMapping public ResponseEntity> listAll() { var tenants = vendorTenantService.listAll(); // Parallel health fetch for active tenants var futures = tenants.stream().map(tenant -> java.util.concurrent.CompletableFuture.supplyAsync(() -> { ServerStatus status = vendorTenantService.getServerStatus(tenant); String licenseExpiry = vendorTenantService .getLicenseForTenant(tenant.getId()) .map(l -> l.getExpiresAt() != null ? l.getExpiresAt().toString() : null) .orElse(null); int agentCount = 0; int environmentCount = 0; int agentLimit = -1; String endpoint = tenant.getServerEndpoint(); boolean isActive = "ACTIVE".equals(tenant.getStatus().name()); if (isActive && endpoint != null && !endpoint.isBlank() && "RUNNING".equals(status.state().name())) { var serverApi = vendorTenantService.getServerApiClient(); agentCount = serverApi.getAgentCount(endpoint); environmentCount = serverApi.getEnvironmentCount(endpoint); } var license = vendorTenantService.getLicenseForTenant(tenant.getId()); if (license.isPresent() && license.get().getLimits() != null) { var limits = license.get().getLimits(); if (limits.containsKey("agents")) { agentLimit = ((Number) limits.get("agents")).intValue(); } } return new VendorTenantSummary( tenant.getId(), tenant.getName(), tenant.getSlug(), tenant.getTier().name(), tenant.getStatus().name(), status.state().name(), licenseExpiry, tenant.getProvisionError(), agentCount, environmentCount, agentLimit ); })).toList(); List summaries = futures.stream() .map(java.util.concurrent.CompletableFuture::join) .toList(); return ResponseEntity.ok(summaries); } ``` - [ ] **Step 3: Expose ServerApiClient from VendorTenantService** Add a getter in `src/main/java/net/siegeln/cameleer/saas/vendor/VendorTenantService.java`: ```java public ServerApiClient getServerApiClient() { return serverApiClient; } ``` (The `serverApiClient` field already exists in VendorTenantService — check around line 30.) - [ ] **Step 4: Verify compilation** Run: `./mvnw compile -pl . -q` Expected: BUILD SUCCESS - [ ] **Step 5: Commit** ```bash git add src/main/java/net/siegeln/cameleer/saas/vendor/VendorTenantController.java \ src/main/java/net/siegeln/cameleer/saas/vendor/VendorTenantService.java git commit -m "feat: add agent/env counts to vendor tenant list endpoint" ``` --- ### Task 2: Update frontend types and columns **Files:** - Modify: `ui/src/types/api.ts` - Modify: `ui/src/pages/vendor/VendorTenantsPage.tsx` - [ ] **Step 1: Add fields to VendorTenantSummary TypeScript type** In `ui/src/types/api.ts`, update the `VendorTenantSummary` interface: ```typescript export interface VendorTenantSummary { id: string; name: string; slug: string; tier: string; status: string; serverState: string; licenseExpiry: string | null; provisionError: string | null; agentCount: number; environmentCount: number; agentLimit: number; } ``` - [ ] **Step 2: Add Agents and Envs columns to VendorTenantsPage** In `ui/src/pages/vendor/VendorTenantsPage.tsx`, add a helper function after `statusColor`: ```typescript function formatUsage(used: number, limit: number): string { return limit < 0 ? `${used} / ∞` : `${used} / ${limit}`; } ``` Then add two column entries in the `columns` array, after the `serverState` column (after line 54) and before the `licenseExpiry` column: ```typescript { key: 'agentCount', header: 'Agents', render: (_v, row) => ( {formatUsage(row.agentCount, row.agentLimit)} ), }, { key: 'environmentCount', header: 'Envs', render: (_v, row) => ( {row.environmentCount} ), }, ``` - [ ] **Step 3: Build the UI** Run: `cd ui && npm run build` Expected: Build succeeds with no errors. - [ ] **Step 4: Commit** ```bash git add ui/src/types/api.ts ui/src/pages/vendor/VendorTenantsPage.tsx git commit -m "feat: show agent/env counts in vendor tenant list" ``` --- ### Task 3: Verify end-to-end - [ ] **Step 1: Run backend tests** Run: `./mvnw test -pl . -q` Expected: All tests pass. (Existing tests use mocks, the new parallel fetch doesn't break them since it only affects the controller's list mapping.) - [ ] **Step 2: Verify in browser** Navigate to the vendor tenant list. Confirm: - "Agents" column shows "0 / ∞" (or actual count if agents are connected) - "Envs" column shows "1" (or actual count) - PROVISIONING/SUSPENDED tenants show "0" for both - 30s auto-refresh still works - [ ] **Step 3: Final commit and push** ```bash git push ```