feat(admin): apix-admin UI at admin.api-index.org
Deploy to Production / deploy (push) Failing after 47s

Internal maintainer dashboard: global stats, sandbox list with
tier-filter and pagination, sandbox drill-down with D3 world map,
tier-promotion form. API-key auth via HttpOnly cookie. Port 8084,
rolling a/b deploy alongside the existing service pairs.

- apix-common: AdminSandboxSummary, AdminSandboxListResponse, AdminStatsResponse DTOs
- apix-registry: AdminResource (4 endpoints), SandboxService.listAllSandboxes + getGlobalStats
- apix-registry: SandboxResource GET /{uuid}/dashboard endpoint (was missing — fixes portal 500)
- apix-portal: RegistryClient path corrected to /dashboard
- apix-admin: new Quarkus module — auth filter, registry REST client, 3 resources, 6 Qute templates, admin.js
- infra: Dockerfile.admin, docker-compose admin-a/b services, Caddyfile admin.api-index.org block
- scripts: deploy-bluegreen.sh extended for admin-a/b

Co-Authored-By: Mira Rehfeld <noreply@anthropic.com>
This commit is contained in:
Carsten Rehfeld
2026-05-16 21:18:48 +02:00
parent e385d05ce1
commit 82a379e536
32 changed files with 1300 additions and 32 deletions
@@ -0,0 +1,10 @@
package org.botstandards.apix.common;
import java.util.List;
public record AdminSandboxListResponse(
long total,
int page,
int size,
List<AdminSandboxSummary> sandboxes
) {}
@@ -0,0 +1,21 @@
package org.botstandards.apix.common;
import com.fasterxml.jackson.annotation.JsonInclude;
import java.time.Instant;
/** Per-sandbox row in the admin list and stats endpoints. */
public record AdminSandboxSummary(
String sandboxId,
String name,
String tier,
int ratePerMinute,
@JsonInclude(JsonInclude.Include.ALWAYS) Integer maxServices,
Instant createdAt,
Instant expiresAt,
boolean expired,
@JsonInclude(JsonInclude.Include.NON_NULL) String location,
@JsonInclude(JsonInclude.Include.NON_NULL) Double lat,
@JsonInclude(JsonInclude.Include.NON_NULL) Double lon,
long serviceCount,
long totalRequests
) {}
@@ -0,0 +1,17 @@
package org.botstandards.apix.common;
import java.util.List;
import java.util.Map;
public record AdminStatsResponse(
long totalSandboxes,
long activeSandboxes,
long expiredSandboxes,
Map<String, Long> byTier,
long totalServices,
long totalRequests,
/** Geo-points for the world map: one point per sandbox that provided a location. */
List<GeoPoint> registrarPoints
) {
public record GeoPoint(double lat, double lon, String name, String tier) {}
}