Files
cameleer-saas/docs/superpowers/plans/2026-04-10-fleet-health-plan.md

211 lines
6.5 KiB
Markdown
Raw Normal View History

# 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
```