Rename Java packages from net.siegeln.cameleer3 to net.siegeln.cameleer, update all references in workflows, Docker configs, docs, and bootstrap. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
6.5 KiB
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:
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:
@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:
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
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:
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:
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:
{
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
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
git push