chore: add missing source modules to version control
Deploy to Production / deploy (push) Failing after 7s

apix-demo, apix-portal/src, apix-spider/src, apix-registry/src,
apix-common/src were never staged. Without them the CI build has no
source to compile and the Docker images cannot be produced.

Also adds docs/ (infrastructure notes) missed in prior commits.

Co-Authored-By: Mira <noreply@anthropic.com>
This commit is contained in:
Carsten Rehfeld
2026-05-14 15:49:03 +02:00
parent a9b3354bde
commit 46f32c2df2
87 changed files with 6657 additions and 34 deletions
@@ -0,0 +1,19 @@
package org.botstandards.apix.portal.client;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import org.botstandards.apix.common.SandboxDashboardResponse;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
@RegisterRestClient(configKey = "registry")
@Path("/sandbox")
@Produces(MediaType.APPLICATION_JSON)
public interface RegistryClient {
@GET
@Path("/{uuid}")
SandboxDashboardResponse getDashboard(@PathParam("uuid") String uuid);
}
@@ -0,0 +1,62 @@
package org.botstandards.apix.portal.resource;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.quarkus.qute.CheckedTemplate;
import io.quarkus.qute.TemplateInstance;
import jakarta.inject.Inject;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import org.botstandards.apix.common.SandboxDashboardResponse;
import org.eclipse.microprofile.rest.client.inject.RestClient;
import org.botstandards.apix.portal.client.RegistryClient;
import org.jboss.logging.Logger;
@Path("/sandbox")
public class DashboardResource {
private static final Logger LOG = Logger.getLogger(DashboardResource.class);
@Inject
@RestClient
RegistryClient registryClient;
@Inject
ObjectMapper objectMapper;
@CheckedTemplate
static class Templates {
static native TemplateInstance dashboard(SandboxDashboardResponse dashboard, String dataJson);
static native TemplateInstance notFound(String uuid);
static native TemplateInstance error(String uuid, String message);
}
@GET
@Path("/{uuid}")
@Produces(MediaType.TEXT_HTML)
public TemplateInstance dashboard(@PathParam("uuid") String uuid) {
SandboxDashboardResponse dashboard;
try {
dashboard = registryClient.getDashboard(uuid);
} catch (WebApplicationException e) {
int status = e.getResponse().getStatus();
if (status == 404) return Templates.notFound(uuid);
if (status == 400) return Templates.notFound(uuid);
LOG.errorf("Registry error fetching sandbox %s: HTTP %d", uuid, status);
return Templates.error(uuid, "Registry unavailable");
} catch (Exception e) {
LOG.errorf(e, "Failed to fetch sandbox %s from registry", uuid);
return Templates.error(uuid, "Could not reach the registry");
}
try {
// Jackson produces safe JSON; replace </ to prevent </script> injection
String raw = objectMapper.writeValueAsString(dashboard);
String safe = raw.replace("</", "<\\/");
return Templates.dashboard(dashboard, safe);
} catch (Exception e) {
throw new WebApplicationException(Response.serverError()
.entity("Failed to serialize dashboard data").build());
}
}
}
@@ -0,0 +1,565 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>API Index — Global Discovery Infrastructure for Autonomous Agents</title>
<meta name="description" content="A machine-readable, always-current index of agent-consumable services. The global discovery infrastructure for autonomous agents.">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--black: #0a0a0a;
--gray: #4a4a4a;
--light: #f5f5f5;
--border: #e0e0e0;
--accent: #1a1a1a;
--mono: "JetBrains Mono", "Fira Code", "Courier New", monospace;
--sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
}
body {
font-family: var(--sans);
background: #ffffff;
color: var(--black);
line-height: 1.6;
min-height: 100vh;
display: flex;
flex-direction: column;
}
header {
border-bottom: 1px solid var(--border);
padding: 1.5rem 2rem;
display: flex;
align-items: baseline;
gap: 1rem;
}
header .wordmark {
font-family: var(--mono);
font-size: 0.95rem;
font-weight: 600;
letter-spacing: 0.02em;
color: var(--black);
text-decoration: none;
}
header .tagline {
font-size: 0.8rem;
color: var(--gray);
}
main {
flex: 1;
max-width: 680px;
margin: 0 auto;
padding: 4rem 2rem;
width: 100%;
}
h1 {
font-size: 1.75rem;
font-weight: 700;
letter-spacing: -0.02em;
line-height: 1.25;
margin-bottom: 1.25rem;
}
.lead {
font-size: 1.05rem;
color: var(--gray);
margin-bottom: 2.5rem;
max-width: 540px;
}
.problem {
background: var(--light);
border-left: 3px solid var(--black);
padding: 1.25rem 1.5rem;
margin-bottom: 2.5rem;
font-size: 0.95rem;
}
.problem p + p {
margin-top: 0.75rem;
}
h2 {
font-size: 0.75rem;
font-weight: 600;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--gray);
margin-bottom: 1rem;
}
.features {
display: grid;
gap: 1.25rem;
margin-bottom: 2.5rem;
}
.feature {
border: 1px solid var(--border);
padding: 1.25rem;
}
.feature .label {
font-family: var(--mono);
font-size: 0.75rem;
font-weight: 600;
color: var(--gray);
margin-bottom: 0.4rem;
}
.feature p {
font-size: 0.9rem;
color: var(--gray);
}
.draft-suite {
border: 1px solid var(--border);
margin-bottom: 2.5rem;
}
.draft-item {
display: flex;
align-items: flex-start;
gap: 1.25rem;
padding: 1rem 1.25rem;
border-bottom: 1px solid var(--border);
}
.draft-item:last-child {
border-bottom: none;
}
.draft-item .label {
font-family: var(--mono);
font-size: 0.7rem;
font-weight: 600;
color: var(--gray);
text-transform: uppercase;
letter-spacing: 0.08em;
white-space: nowrap;
padding-top: 0.15rem;
min-width: 120px;
flex-shrink: 0;
}
.draft-item .content {
font-size: 0.9rem;
min-width: 0;
overflow-wrap: break-word;
word-break: break-word;
}
.draft-item .content a {
color: var(--black);
text-decoration: underline;
text-underline-offset: 3px;
}
.draft-item .content a:hover {
color: var(--gray);
}
.draft-item .content .sub {
font-size: 0.8rem;
color: var(--gray);
margin-top: 0.25rem;
}
.draft-item.superseded {
background: var(--light);
}
.draft-item.superseded .label {
color: #aaa;
}
.draft-item.superseded .content a {
color: var(--gray);
}
.status {
margin-bottom: 2.5rem;
}
.status-line {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.6rem 0;
border-bottom: 1px solid var(--border);
font-size: 0.875rem;
}
.status-line:first-child {
border-top: 1px solid var(--border);
}
.dot {
width: 6px;
height: 6px;
border-radius: 50%;
flex-shrink: 0;
}
.dot.done { background: #22c55e; }
.dot.active { background: #f59e0b; }
.dot.pending { background: var(--border); }
.status-line .item { flex: 1; }
.status-line .phase {
font-size: 0.75rem;
color: var(--gray);
font-family: var(--mono);
}
.contact {
font-size: 0.875rem;
color: var(--gray);
}
.contact a {
color: var(--black);
text-decoration: underline;
text-underline-offset: 3px;
}
footer {
border-top: 1px solid var(--border);
padding: 1.25rem 2rem;
font-size: 0.75rem;
color: var(--gray);
display: flex;
justify-content: space-between;
flex-wrap: wrap;
gap: 0.5rem;
}
footer a {
color: var(--gray);
text-decoration: underline;
text-underline-offset: 3px;
}
@media (max-width: 600px) {
header { flex-direction: column; gap: 0.25rem; }
h1 { font-size: 1.35rem; }
main { padding: 2.5rem 1.25rem; }
.draft-block { flex-direction: column; gap: 0.5rem; }
}
</style>
</head>
<body>
<header>
<a class="wordmark" href="/">api-index.org</a>
<span class="tagline">open service registry for autonomous agents</span>
</header>
<main>
<h1>The global discovery infrastructure<br>for autonomous agents.</h1>
<p class="lead">
Autonomous agents cannot reliably find the services they need.
The internet was built for humans — its discovery infrastructure
assumes a human reading a screen. Agents are navigating it blind.
</p>
<div class="problem">
<p>
The API Index is a single, globally queryable, machine-readable
index of agent-consumable API services — with a structured trust model,
capability-based search, and a stable entry point any agent can start from.
</p>
<p>
Discovery is always free for consuming agents. The index is governed by
the <a href="https://botstandards.org" target="_blank" rel="noopener noreferrer">Bot Standards Foundation</a>, a neutral
non-profit Swiss Stiftung.
</p>
</div>
<h2>What the index provides</h2>
<div class="features">
<div class="feature">
<div class="label">Single entry point</div>
<p>One stable global URL. Any agent navigates the full index
from here via HATEOAS hypermedia links — no prior knowledge required.</p>
</div>
<div class="feature">
<div class="label">Capability search</div>
<p>Find services by what they do — not by name or URL.
Structured taxonomy from <code>data.legal</code> to <code>nlp</code>
to <code>iot</code> and beyond.</p>
</div>
<div class="feature">
<div class="label">Three-dimensional trust model</div>
<p>Verified organisation identity · Automated service verification ·
Continuous liveness monitoring. Agents apply their own trust policy
against verifiable metadata.</p>
</div>
<div class="feature">
<div class="label">Open standard</div>
<p>APIX Manifest (APM) supports OpenAPI, MCP, AsyncAPI, and
GraphQL. Published under open licence. No proprietary formats.</p>
</div>
</div>
<h2>IETF Internet-Drafts</h2>
<div class="draft-suite">
<div class="draft-item">
<span class="label">Core</span>
<div class="content">
<a href="https://datatracker.ietf.org/doc/draft-rehfeld-apix-core/" target="_blank" rel="noopener noreferrer">
draft-rehfeld-apix-core
</a>
<div class="sub">Core infrastructure, trust model, Index API, operator governance · April 2026</div>
</div>
</div>
<div class="draft-item">
<span class="label">Services</span>
<div class="content">
<a href="https://datatracker.ietf.org/doc/draft-rehfeld-apix-services/" target="_blank" rel="noopener noreferrer">
draft-rehfeld-apix-services
</a>
<div class="sub">Web API and bot service registration profile, capability taxonomy, notification channels · April 2026</div>
</div>
</div>
<div class="draft-item">
<span class="label">IoT</span>
<div class="content">
<a href="https://datatracker.ietf.org/doc/draft-rehfeld-apix-iot/" target="_blank" rel="noopener noreferrer">
draft-rehfeld-apix-iot
</a>
<div class="sub">IoT device class and instance registration, presence signalling, agent delegation · April 2026</div>
</div>
</div>
<div class="draft-item superseded">
<span class="label">Superseded</span>
<div class="content">
<a href="https://datatracker.ietf.org/doc/draft-rehfeld-bot-service-index/" target="_blank" rel="noopener noreferrer">
draft-rehfeld-bot-service-index
</a>
<div class="sub">Supersession notice for prior revision · redirects to the three drafts above</div>
</div>
</div>
</div>
<h2>Current status</h2>
<div class="status">
<div class="status-line">
<span class="dot done"></span>
<span class="item">Internet-Draft submitted to IETF</span>
<span class="phase">Phase 0</span>
</div>
<div class="status-line">
<span class="dot done"></span>
<span class="item">IETF Dispatch posted · community engagement underway · May 2026</span>
<span class="phase">Phase 0</span>
</div>
<div class="status-line">
<span class="dot active"></span>
<span class="item">Bot Standards Foundation incorporation underway</span>
<span class="phase">Phase 1</span>
</div>
<div class="status-line">
<span class="dot pending"></span>
<span class="item">Founding member programme open</span>
<span class="phase">Phase 2</span>
</div>
<div class="status-line">
<span class="dot done"></span>
<span class="item">Reference implementation live · public sandbox available</span>
<span class="phase">Phase 3</span>
</div>
<div class="status-line">
<span class="dot done"></span>
<span class="item">Index live at api-index.org · May 2026</span>
<span class="phase">Phase 3</span>
</div>
</div>
<h2>Try it</h2>
<div class="draft-suite" style="margin-bottom:2.5rem">
<div class="draft-item">
<span class="label">API root</span>
<div class="content">
<a href="https://api-index.org/" target="_blank" rel="noopener noreferrer">api-index.org/</a>
<div class="sub">HATEOAS entry point — any agent or HTTP client can start here.</div>
</div>
</div>
<div class="draft-item">
<span class="label">Sandbox</span>
<div class="content">
<code>POST api-index.org/sandbox/register</code>
<div class="sub">Register an isolated test namespace. No KYC. Returns a sandbox UUID and API key immediately. Name is a display label — not unique, not used for routing. Body: <code>{"name":"my-agent","contactEmail":"you@example.com"}</code></div>
</div>
</div>
<div class="draft-item">
<span class="label">OpenAPI</span>
<div class="content">
<a href="https://api-index.org/q/openapi" target="_blank" rel="noopener noreferrer">api-index.org/q/openapi</a>
<div class="sub">Full OpenAPI specification for the registry API.</div>
</div>
</div>
</div>
<h2>How to integrate APIX into your agent</h2>
<div class="problem" style="margin-bottom:1.5rem">
<p>
APIX is designed so that a well-instructed agent can navigate it without
any prior knowledge — starting from a single URL and following hypermedia
links. The minimal system prompt addition is one sentence:
</p>
</div>
<div class="draft-suite" style="margin-bottom:1rem">
<div class="draft-item" style="background:#f9f9f9">
<span class="label">Prompt</span>
<div class="content" style="font-family:'JetBrains Mono','Fira Code','Courier New',monospace;font-size:0.8rem;line-height:1.6;color:#1a1a1a">
When you need an external service, first query the API Index at
https://api-index.org/ — follow _links.servicesSearch with a
?capability= parameter that describes what you need.
If you use a sandbox, submit feedback at
_links.sandbox + /feedback after each session.
</div>
</div>
</div>
<p style="font-size:0.85rem;color:var(--gray);margin-bottom:2.5rem">
That is the full integration for a consuming agent. No SDK, no API key, no registration required.
The HATEOAS root tells the agent where everything is; the agent does not need to be told in advance.
</p>
<h2>The Bot Service Manifest (BSM)</h2>
<p style="font-size:0.875rem;color:var(--gray);margin-bottom:1.25rem">
Every registered service carries a structured manifest that agents read to decide
whether to invoke a service. The fields are designed to be self-describing —
the OpenAPI schema at <a href="https://api-index.org/q/openapi" style="color:var(--black)" target="_blank" rel="noopener noreferrer">api-index.org/q/openapi</a>
contains full field descriptions that an agent can retrieve and reason over without
being told what each field means in its system prompt.
</p>
<div class="draft-suite" style="margin-bottom:1rem">
<div class="draft-item">
<span class="label">capabilities</span>
<div class="content">
<div class="sub" style="color:var(--black);font-size:0.875rem">Structured taxonomy strings — the primary search key. Examples: <code>nlp.translation</code>, <code>iot.telemetry</code>, <code>data.legal</code>.</div>
</div>
</div>
<div class="draft-item">
<span class="label">endpoint</span>
<div class="content">
<div class="sub" style="color:var(--black);font-size:0.875rem">The service URL an agent calls. Must be HTTPS. The liveness spider checks this continuously.</div>
</div>
</div>
<div class="draft-item">
<span class="label">openApiSpecUrl</span>
<div class="content">
<div class="sub" style="color:var(--black);font-size:0.875rem">URL of the OpenAPI specification. An agent fetches and parses this to learn the service's operations, parameters, and schemas — no human documentation required.</div>
</div>
</div>
<div class="draft-item">
<span class="label">mcpSpecUrl</span>
<div class="content">
<div class="sub" style="color:var(--black);font-size:0.875rem">URL of the MCP server manifest. Equivalent machine-readable contract for agents using the Model Context Protocol. Either field — or both — may be present.</div>
</div>
</div>
<div class="draft-item">
<span class="label">serviceStage</span>
<div class="content">
<div class="sub" style="color:var(--black);font-size:0.875rem"><code>PRODUCTION</code> · <code>BETA</code> · <code>EXPERIMENTAL</code> · <code>DEPRECATED</code> · <code>DECOMMISSIONED</code>. Agents should filter by stage based on the risk tolerance of their task.</div>
</div>
</div>
<div class="draft-item">
<span class="label">O-level</span>
<div class="content">
<div class="sub" style="color:var(--black);font-size:0.875rem">Operator verification tier: O-0 (unverified) through O-5 (independently audited). Agents apply their own trust policy against this score.</div>
</div>
</div>
<div class="draft-item">
<span class="label">extensions</span>
<div class="content">
<div class="sub" style="color:var(--black);font-size:0.875rem">Free-form key/value properties for information not covered by standard fields. Query with <code>?property=key:value</code>. Examples: <code>region:eu</code>, <code>dataResidency:DE</code>, <code>agentFramework:langchain</code>.</div>
</div>
</div>
</div>
<p style="font-size:0.85rem;color:var(--gray);margin-bottom:2.5rem">
The <code>extensions</code> field exists for cases where the standard fields are not sufficient.
If your agents consistently use extensions for information that should be a first-class field,
that is a signal that the BSM schema needs to evolve.
The feedback mechanism exists precisely to collect this signal.
</p>
<h2>The feedback loop</h2>
<div class="problem" style="margin-bottom:1.5rem">
<p>
APIX is a living standard. The feedback endpoint at
<code>/sandbox/{name}/feedback</code> lets agents rate their experience —
discovery accuracy, liveness signal quality, error message clarity,
and whether standard BSM fields were sufficient or custom extensions
had to fill the gap.
</p>
<p style="margin-top:0.75rem">
<strong>The design intent:</strong> a well-built agent navigates to the feedback schema
at <code>_links.feedbackSchema</code> from the index root, reads the dimensions,
and submits feedback autonomously after each sandbox session —
without being explicitly told to do so.
If your agent does not do this automatically, add the feedback URL
to its instructions. Low scores on <code>extension_property_coverage</code>
are the clearest signal that the BSM schema has a gap.
</p>
</div>
<div class="draft-suite" style="margin-bottom:2.5rem">
<div class="draft-item">
<span class="label">Schema</span>
<div class="content">
<a href="https://api-index.org/sandbox/feedback-schema" target="_blank" rel="noopener noreferrer">api-index.org/sandbox/feedback-schema</a>
<div class="sub">Rated dimensions with question text and scale labels. Agents read this before submitting.</div>
</div>
</div>
<div class="draft-item">
<span class="label">Submit</span>
<div class="content">
<code>POST /sandbox/{name}/feedback</code>
<div class="sub">No authentication required. Any agent that has used the sandbox can submit. JSON body: <code>{"scores": {"hateoas_navigation": 8, "discovery_accuracy": 7}}</code></div>
</div>
</div>
<div class="draft-item">
<span class="label">Prompt</span>
<div class="content" style="font-family:'JetBrains Mono','Fira Code','Courier New',monospace;font-size:0.8rem;line-height:1.6;color:#1a1a1a">
After using an APIX sandbox, fetch
https://api-index.org/sandbox/feedback-schema,
rate your experience on each dimension (010),
and POST your scores to /sandbox/{name}/feedback.
</div>
</div>
</div>
<p class="contact">
Founding member enquiries and institutional partnerships:
<a href="mailto:carsten@botstandards.org" target="_blank" rel="noopener noreferrer">carsten@botstandards.org</a>
</p>
</main>
<footer>
<span>API Index · governed by the <a href="https://botstandards.org" target="_blank" rel="noopener noreferrer">Bot Standards Foundation</a></span>
<span>IETF · <a href="https://datatracker.ietf.org/doc/draft-rehfeld-apix-core/" target="_blank" rel="noopener noreferrer">apix-core</a> · <a href="https://datatracker.ietf.org/doc/draft-rehfeld-apix-services/" target="_blank" rel="noopener noreferrer">apix-services</a> · <a href="https://datatracker.ietf.org/doc/draft-rehfeld-apix-iot/" target="_blank" rel="noopener noreferrer">apix-iot</a></span>
</footer>
</body>
</html>
@@ -0,0 +1,7 @@
quarkus.http.port=8081
quarkus.smallrye-health.root-path=/q/health
quarkus.log.level=${LOG_LEVEL:INFO}
quarkus.rest-client.registry.url=${APIX_REGISTRY_URL:https://api-index.org}
quarkus.rest-client.registry.connect-timeout=3000
quarkus.rest-client.registry.read-timeout=5000
@@ -0,0 +1,295 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{dashboard.name} · APIX Sandbox</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
background: #0d1117;
color: #c9d1d9;
font-family: 'SF Mono', 'Consolas', 'Fira Code', monospace;
font-size: 14px;
line-height: 1.6;
}
a { color: #58a6ff; text-decoration: none; }
a:hover { text-decoration: underline; }
/* ── Header ── */
.header {
display: flex;
align-items: center;
gap: 1rem;
padding: 1rem 1.5rem;
border-bottom: 1px solid #21262d;
}
.header-logo { color: #8b949e; font-size: 0.8rem; }
.header-name { font-size: 1rem; color: #e6edf3; font-weight: 600; }
.tier-badge {
background: #161b22;
border: 1px solid #30363d;
color: #8b949e;
padding: 0.1rem 0.5rem;
border-radius: 12px;
font-size: 0.7rem;
letter-spacing: 0.05em;
}
/* ── Map ── */
#map-wrap {
background: #060d18;
position: relative;
overflow: hidden;
}
#world-map {
display: block;
width: 100%;
}
.map-label {
position: absolute;
bottom: 0.6rem;
right: 0.8rem;
font-size: 0.65rem;
color: #30363d;
letter-spacing: 0.04em;
}
/* ── Star ── */
@keyframes star-glow {
0%, 100% { filter: drop-shadow(0 0 3px #ffd700) drop-shadow(0 0 6px #ffd70066); }
50% { filter: drop-shadow(0 0 8px #ffd700) drop-shadow(0 0 18px #ffd700aa) drop-shadow(0 0 32px #ffd70033); }
}
.registrar-star {
font-size: 14px;
fill: #ffd700;
dominant-baseline: central;
text-anchor: middle;
animation: star-glow 2.4s ease-in-out infinite;
cursor: default;
}
/* ── Blinks ── */
@keyframes blink-pulse {
0% { opacity: 0; r: 2; }
25% { opacity: 0.9; r: 5; }
65% { opacity: 0.4; r: 4; }
100% { opacity: 0; r: 2; }
}
.agent-blink {
fill: #3d8bfd;
opacity: 0;
animation: blink-pulse 2.8s ease-in-out infinite;
}
/* ── Stats ── */
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1px;
background: #21262d;
border-top: 1px solid #21262d;
}
.stat-card {
background: #0d1117;
padding: 1rem 1.5rem;
}
.stat-label {
font-size: 0.7rem;
color: #484f58;
letter-spacing: 0.06em;
text-transform: uppercase;
margin-bottom: 0.3rem;
}
.stat-value {
font-size: 1.4rem;
color: #e6edf3;
font-weight: 600;
}
.stat-sub {
font-size: 0.7rem;
color: #484f58;
margin-top: 0.2rem;
}
/* ── Footer ── */
.footer {
padding: 1rem 1.5rem;
border-top: 1px solid #21262d;
font-size: 0.75rem;
color: #484f58;
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 0.5rem;
}
</style>
</head>
<body>
<div class="header">
<span class="header-logo"><a href="/">APIX</a></span>
<span class="header-name">{dashboard.name}</span>
<span class="tier-badge">{dashboard.tier}</span>
</div>
<div id="map-wrap">
<svg id="world-map"></svg>
<div class="map-label">agent interaction map</div>
</div>
<div class="stats-grid">
<div class="stat-card">
<div class="stat-label">Services registered</div>
<div class="stat-value">{#if dashboard.usage.get('SERVICE_REGISTERED') != null}{dashboard.usage.get('SERVICE_REGISTERED')}{#else}0{/if}</div>
<div class="stat-sub">of {#if dashboard.maxServices != null}{dashboard.maxServices}{#else}∞{/if} allowed</div>
</div>
<div class="stat-card">
<div class="stat-label">Capability searches</div>
<div class="stat-value">{#if dashboard.usage.get('SERVICE_SEARCHED') != null}{dashboard.usage.get('SERVICE_SEARCHED')}{#else}0{/if}</div>
<div class="stat-sub">agent discovery calls</div>
</div>
<div class="stat-card">
<div class="stat-label">Service list calls</div>
<div class="stat-value">{#if dashboard.usage.get('SERVICE_LISTED') != null}{dashboard.usage.get('SERVICE_LISTED')}{#else}0{/if}</div>
<div class="stat-sub">full list requests</div>
</div>
<div class="stat-card">
<div class="stat-label">Rate limit</div>
<div class="stat-value">{dashboard.ratePerMinute}</div>
<div class="stat-sub">requests / minute</div>
</div>
<div class="stat-card">
<div class="stat-label">Sandbox expires</div>
<div class="stat-value" id="expires-val"></div>
<div class="stat-sub" id="expires-sub">{dashboard.expiresAt}</div>
</div>
{#if dashboard.registrarLocation != null}
<div class="stat-card">
<div class="stat-label">Registered from</div>
<div class="stat-value" style="font-size:1rem;">{dashboard.registrarLocation}</div>
<div class="stat-sub">owner-declared location</div>
</div>
{/if}
</div>
<div class="footer">
<span>sandbox id: {dashboard.sandboxId}</span>
<span><a href="https://api-index.org">api-index.org</a> · APIX Registry</span>
</div>
<!-- Data injected by portal (Jackson-serialised, </script> escaped) -->
<script>
var __D = {dataJson.raw};
</script>
<script src="https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/topojson-client@3/dist/topojson-client.min.js"></script>
{#raw}
<script>
(function () {
var data = window.__D || {};
var visits = data.recentVisits || [];
var hasRegistrar = typeof data.registrarLat === 'number' && typeof data.registrarLon === 'number';
// ── Expires display ──────────────────────────────────────────────────────
var expiresEl = document.getElementById('expires-val');
var expiresSub = document.getElementById('expires-sub');
if (data.expiresAt && expiresEl) {
var d = new Date(data.expiresAt);
expiresEl.textContent = d.toLocaleDateString('en-GB', { day: 'numeric', month: 'short', year: 'numeric' });
var days = Math.ceil((d - Date.now()) / 86400000);
expiresSub.textContent = days > 0 ? days + ' days remaining' : 'expired';
}
// ── Map ──────────────────────────────────────────────────────────────────
var wrap = document.getElementById('map-wrap');
var svg = d3.select('#world-map');
var W = wrap.clientWidth || 800;
var H = Math.round(W * 0.48);
svg.attr('viewBox', '0 0 ' + W + ' ' + H)
.attr('width', W)
.attr('height', H);
var projection = d3.geoNaturalEarth1()
.scale(W / 6.3)
.translate([W / 2, H / 2]);
var path = d3.geoPath(projection);
// Ocean
svg.append('rect')
.attr('width', W).attr('height', H)
.attr('fill', '#060d18');
fetch('https://cdn.jsdelivr.net/npm/world-atlas@2/countries-110m.json')
.then(function(r) { return r.json(); })
.then(function(world) {
// Land
svg.append('path')
.datum(topojson.feature(world, world.objects.countries))
.attr('d', path)
.attr('fill', '#111820')
.attr('stroke', '#1e2a38')
.attr('stroke-width', 0.5);
// Graticule (faint grid)
svg.append('path')
.datum(d3.geoGraticule()())
.attr('d', path)
.attr('fill', 'none')
.attr('stroke', '#0d1a28')
.attr('stroke-width', 0.4);
// Agent blinks — resolve duplicates by approximate cell
var cellSize = 1.5; // degrees
var cells = {};
visits.forEach(function(v) {
var cell = Math.round(v.lat / cellSize) + ',' + Math.round(v.lon / cellSize);
if (!cells[cell]) cells[cell] = { lat: v.lat, lon: v.lon, count: 0 };
cells[cell].count++;
});
var points = Object.values(cells);
points.forEach(function(pt, i) {
var pos = projection([pt.lon, pt.lat]);
if (!pos) return;
var r = Math.min(2 + Math.log1p(pt.count) * 1.5, 8);
svg.append('circle')
.attr('class', 'agent-blink')
.attr('cx', pos[0])
.attr('cy', pos[1])
.attr('r', r)
.style('animation-delay', (i * 190 % 5000) + 'ms');
});
// Registrar star — drawn last so it sits on top
if (hasRegistrar) {
var pos = projection([data.registrarLon, data.registrarLat]);
if (pos) {
svg.append('text')
.attr('class', 'registrar-star')
.attr('x', pos[0])
.attr('y', pos[1])
.text('★');
}
}
})
.catch(function(e) {
console.warn('Map load failed:', e);
});
// ── Resize ────────────────────────────────────────────────────────────────
window.addEventListener('resize', function() {
W = wrap.clientWidth || 800;
H = Math.round(W * 0.48);
svg.attr('viewBox', '0 0 ' + W + ' ' + H)
.attr('width', W).attr('height', H);
projection.scale(W / 6.3).translate([W / 2, H / 2]);
svg.selectAll('path').attr('d', path);
});
})();
</script>
{/raw}
</body>
</html>
@@ -0,0 +1,24 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Error · APIX</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { background: #0d1117; color: #c9d1d9; font-family: 'SF Mono', 'Consolas', monospace; display: flex; align-items: center; justify-content: center; min-height: 100vh; }
.container { text-align: center; max-width: 480px; padding: 2rem; }
h1 { font-size: 1.2rem; color: #8b949e; margin-bottom: 1rem; font-weight: 400; }
p { font-size: 0.9rem; color: #484f58; margin-bottom: 2rem; }
a { color: #58a6ff; text-decoration: none; font-size: 0.85rem; }
a:hover { text-decoration: underline; }
</style>
</head>
<body>
<div class="container">
<h1>{message}</h1>
<p>Could not load dashboard for sandbox {uuid}.</p>
<a href="/">← back</a>
</div>
</body>
</html>
@@ -0,0 +1,25 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Sandbox not found · APIX</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { background: #0d1117; color: #c9d1d9; font-family: 'SF Mono', 'Consolas', monospace; display: flex; align-items: center; justify-content: center; min-height: 100vh; }
.container { text-align: center; max-width: 480px; padding: 2rem; }
h1 { font-size: 1.2rem; color: #8b949e; margin-bottom: 1rem; font-weight: 400; }
p { font-size: 0.9rem; color: #484f58; margin-bottom: 2rem; line-height: 1.6; }
a { color: #58a6ff; text-decoration: none; font-size: 0.85rem; }
a:hover { text-decoration: underline; }
code { background: #161b22; padding: 0.15em 0.4em; border-radius: 3px; font-size: 0.8rem; color: #8b949e; }
</style>
</head>
<body>
<div class="container">
<h1>Sandbox not found</h1>
<p>No sandbox exists for <code>{uuid}</code>.<br>The sandbox may have expired or the UUID is incorrect.</p>
<a href="/">← back to api-index.org</a>
</div>
</body>
</html>