Files
apix-mvp/apix-portal/src/main/resources/templates/ServiceBrowserResource/browse.html
T
Carsten Rehfeld 89aa2c47cb
Deploy to Production / deploy (push) Successful in 3m16s
portal: add human-navigable service browser (GET /browse)
Registry: new GET /services/browse endpoint — paginated, no auth required,
returns ServiceBrowsePage (total, page, size, items). ServiceSummaryDto
extended with description, registrantName, registrantJurisdiction; lastCheckedAt
removed (not stored). ServiceBrowsePage added to apix-common as shared type.

Portal: ServiceBrowserResource at /browse (list + /browse/{id} detail);
ServiceRegistryClient for the new endpoints; PortalServiceView DTO for detail
deserialization; three Qute templates (browse, detail, error) in the existing
dark-theme design. Landing page links to /browse in the "Try it" section.

Architecture: registry stays pure JSON API; portal is the HTML layer over the
same data — two subdomains, no content negotiation required.

Co-Authored-By: Mira Rehfeld <noreply@anthropic.com>
2026-05-19 10:52:37 +02:00

303 lines
7.7 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>APIX Registry · Browse Services</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-title { font-size: 1rem; color: #e6edf3; font-weight: 600; }
/* ── Search ── */
.search-bar {
padding: 1.25rem 1.5rem;
border-bottom: 1px solid #21262d;
display: flex;
gap: 0.6rem;
align-items: center;
}
.search-input {
flex: 1;
background: #161b22;
border: 1px solid #30363d;
color: #c9d1d9;
padding: 0.5rem 0.75rem;
font-family: inherit;
font-size: 13px;
border-radius: 4px;
outline: none;
}
.search-input:focus { border-color: #58a6ff; }
.search-input::placeholder { color: #484f58; }
.btn {
background: #21262d;
border: 1px solid #30363d;
color: #c9d1d9;
padding: 0.45rem 0.9rem;
font-family: inherit;
font-size: 13px;
cursor: pointer;
border-radius: 4px;
white-space: nowrap;
}
.btn:hover { border-color: #58a6ff; color: #58a6ff; }
.btn-link { background: transparent; border: none; color: #484f58; cursor: pointer; font-family: inherit; font-size: 13px; }
.btn-link:hover { color: #58a6ff; }
/* ── Stats bar ── */
.stats-bar {
padding: 0.45rem 1.5rem;
font-size: 0.7rem;
color: #484f58;
border-bottom: 1px solid #21262d;
letter-spacing: 0.03em;
}
/* ── Service list ── */
.service-list {
display: flex;
flex-direction: column;
gap: 1px;
background: #21262d;
}
.service-card {
background: #0d1117;
padding: 1rem 1.5rem;
display: grid;
grid-template-columns: 1fr auto;
gap: 0.75rem;
align-items: start;
cursor: pointer;
transition: background 0.1s;
}
.service-card:hover { background: #161b22; }
.card-name {
color: #e6edf3;
font-weight: 600;
font-size: 0.95rem;
}
.card-name a { color: #e6edf3; }
.card-name a:hover { color: #58a6ff; text-decoration: none; }
.card-desc {
color: #8b949e;
font-size: 0.78rem;
margin-top: 0.2rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 72ch;
}
.caps {
display: flex;
flex-wrap: wrap;
gap: 0.25rem;
margin-top: 0.5rem;
}
.cap-tag {
background: #161b22;
border: 1px solid #30363d;
color: #8b949e;
padding: 0.05rem 0.4rem;
border-radius: 3px;
font-size: 0.65rem;
letter-spacing: 0.02em;
}
.card-meta {
font-size: 0.68rem;
color: #484f58;
margin-top: 0.45rem;
display: flex;
align-items: center;
gap: 0.4rem;
}
/* ── Liveness dot ── */
.dot {
width: 7px;
height: 7px;
border-radius: 50%;
display: inline-block;
flex-shrink: 0;
}
.dot-LIVE { background: #3fb950; box-shadow: 0 0 5px #3fb95088; }
.dot-DEGRADED { background: #d29922; }
.dot-UNREACHABLE { background: #f85149; }
.dot-PENDING { background: #484f58; }
/* ── O-level badge ── */
.o-badge {
font-size: 0.62rem;
padding: 0.15rem 0.5rem;
border-radius: 10px;
border: 1px solid;
white-space: nowrap;
letter-spacing: 0.04em;
text-transform: uppercase;
align-self: flex-start;
margin-top: 0.15rem;
}
.o-UNVERIFIED { color: #484f58; border-color: #30363d; background: transparent; }
.o-IDENTITY_VERIFIED { color: #d29922; border-color: #d2992255; background: #d2992211; }
.o-LEGAL_ENTITY_VERIFIED { color: #e3b341; border-color: #e3b34155; background: #e3b34111; }
.o-HYGIENE_VERIFIED { color: #58a6ff; border-color: #58a6ff55; background: #58a6ff11; }
.o-OPERATIONALLY_VERIFIED { color: #3fb950; border-color: #3fb95055; background: #3fb95011; }
.o-AUDITED { color: #bc8cff; border-color: #bc8cff55; background: #bc8cff11; }
/* ── Pagination ── */
.pagination {
padding: 1rem 1.5rem;
display: flex;
justify-content: space-between;
align-items: center;
border-top: 1px solid #21262d;
font-size: 0.78rem;
color: #484f58;
}
.page-btn { display: inline-block; color: #8b949e; border: 1px solid #30363d; padding: 0.3rem 0.65rem; border-radius: 4px; }
.page-btn:hover { border-color: #58a6ff; color: #58a6ff; text-decoration: none; }
.page-btn-off { color: #30363d; border: 1px solid #21262d; padding: 0.3rem 0.65rem; border-radius: 4px; }
/* ── Empty state ── */
.empty {
padding: 4rem 1.5rem;
text-align: center;
color: #484f58;
}
.empty h2 { font-size: 1rem; margin-bottom: 0.5rem; color: #30363d; }
/* ── Footer ── */
.footer {
padding: 1rem 1.5rem;
border-top: 1px solid #21262d;
font-size: 0.7rem;
color: #484f58;
display: flex;
justify-content: space-between;
flex-wrap: wrap;
gap: 0.5rem;
}
</style>
</head>
<body>
<div class="header">
<span class="header-logo"><a href="/browse">APIX</a></span>
<span class="header-title">Service Registry</span>
</div>
<form class="search-bar" method="get" action="/browse">
<input class="search-input" type="text" name="capability" value="{capability}"
placeholder="search by capability (e.g. nlp, translation, speech-to-text)"
autocomplete="off" spellcheck="false">
<button class="btn" type="submit">Search</button>
{#if capability.isEmpty() == false}
<a href="/browse" class="btn-link">clear</a>
{/if}
</form>
<div class="stats-bar">
{page.total} service{#if page.total != 1}s{/if}
{#if capability.isEmpty()}in production · public registry{#else}matching «{capability}»{/if}
</div>
{#if page.items.isEmpty()}
<div class="empty">
<h2>no services found</h2>
<p>
{#if capability.isEmpty()}
No production services are registered yet.
{#else}
No production services match capability «{capability}».
{/if}
</p>
</div>
{#else}
<div class="service-list">
{#for s in page.items}
<div class="service-card" onclick="location.href='/browse/{s.id}'">
<div>
<div class="card-name"><a href="/browse/{s.id}">{s.name}</a></div>
{#if s.description != null}
<div class="card-desc">{s.description}</div>
{/if}
{#if s.capabilities != null && s.capabilities.isEmpty() == false}
<div class="caps">
{#for cap in s.capabilities}
<span class="cap-tag">{cap}</span>
{/for}
</div>
{/if}
<div class="card-meta">
<span class="dot dot-{s.livenessStatus}"></span>
<span>{s.livenessStatus}</span>
{#if s.registrantName != null}
<span>·</span>
<span>{s.registrantName}</span>
{/if}
{#if s.registrantJurisdiction != null}
<span>·</span>
<span>{s.registrantJurisdiction}</span>
{/if}
</div>
</div>
<div>
<span class="o-badge o-{s.oLevel}">{s.oLevel}</span>
</div>
</div>
{/for}
</div>
{/if}
<div class="pagination">
<span>
{#if hasPrev}
<a href="/browse?page={prevPage}{#if capability.isEmpty() == false}&capability={capability}{/if}" class="page-btn">← prev</a>
{#else}
<span class="page-btn-off">← prev</span>
{/if}
</span>
<span>page {currentPage + 1} of {totalPages}</span>
<span>
{#if hasNext}
<a href="/browse?page={nextPage}{#if capability.isEmpty() == false}&capability={capability}{/if}" class="page-btn">next →</a>
{#else}
<span class="page-btn-off">next →</span>
{/if}
</span>
</div>
<div class="footer">
<span>APIX Registry · open protocol</span>
<span>
<a href="https://api-index.org">api-index.org</a> ·
<a href="https://api-index.org/q/openapi">OpenAPI</a>
</span>
</div>
</body>
</html>