211 lines
6.5 KiB
Markdown
211 lines
6.5 KiB
Markdown
|
|
# 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
|
||
|
|
```
|