# 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<List<VendorTenantSummary>> 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<VendorTenantSummary> 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) => (
      <span style={{ fontFamily: 'monospace', fontSize: '0.875rem' }}>
        {formatUsage(row.agentCount, row.agentLimit)}
      </span>
    ),
  },
  {
    key: 'environmentCount',
    header: 'Envs',
    render: (_v, row) => (
      <span style={{ fontFamily: 'monospace', fontSize: '0.875rem' }}>
        {row.environmentCount}
      </span>
    ),
  },
```

- [ ] **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
```
