ad19c8f20f
Deploy to Production / deploy (push) Successful in 3m14s
requireTypeSafeExpressions=false allows {page+1} and (page+1)*size
in pagination conditions. JS template literals with ${days} replaced
with string concatenation to avoid Qute consuming the {days} token.
Co-Authored-By: Mira Rehfeld <noreply@anthropic.com>
125 lines
4.6 KiB
HTML
125 lines
4.6 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>Sandboxes · APIX Admin</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: 13px; }
|
|
a { color: #58a6ff; text-decoration: none; }
|
|
a:hover { text-decoration: underline; }
|
|
nav {
|
|
display: flex; align-items: center; gap: 1.5rem;
|
|
padding: 0.75rem 1.5rem; border-bottom: 1px solid #21262d; background: #161b22;
|
|
}
|
|
.nav-logo { color: #8b949e; font-size: 0.75rem; }
|
|
.nav-logo span { color: #e6edf3; font-weight: 600; font-size: 0.9rem; }
|
|
.nav-link { font-size: 0.8rem; color: #8b949e; }
|
|
.nav-link.active, .nav-link:hover { color: #e6edf3; }
|
|
.nav-right { margin-left: auto; }
|
|
|
|
.toolbar {
|
|
padding: 0.75rem 1.5rem; border-bottom: 1px solid #21262d;
|
|
display: flex; gap: 0.75rem; align-items: center; flex-wrap: wrap;
|
|
}
|
|
.toolbar-label { font-size: 0.7rem; color: #484f58; }
|
|
.filter-link {
|
|
font-size: 0.75rem; color: #8b949e;
|
|
padding: 0.2rem 0.6rem; border: 1px solid #30363d; border-radius: 12px;
|
|
}
|
|
.filter-link:hover, .filter-link.active { color: #e6edf3; border-color: #58a6ff; }
|
|
.count { margin-left: auto; font-size: 0.75rem; color: #484f58; }
|
|
|
|
table { width: 100%; border-collapse: collapse; }
|
|
th {
|
|
text-align: left; padding: 0.6rem 1.5rem;
|
|
font-size: 0.65rem; color: #484f58; text-transform: uppercase; letter-spacing: 0.06em;
|
|
border-bottom: 1px solid #21262d; background: #161b22;
|
|
}
|
|
td { padding: 0.55rem 1.5rem; border-bottom: 1px solid #161b22; vertical-align: middle; }
|
|
tr:hover td { background: #161b22; }
|
|
|
|
.tier-badge {
|
|
background: #0d1117; border: 1px solid #30363d;
|
|
color: #8b949e; padding: 0.1rem 0.4rem; border-radius: 10px;
|
|
font-size: 0.65rem; letter-spacing: 0.04em;
|
|
}
|
|
.tier-COMMUNITY { border-color: #1f6feb; color: #58a6ff; }
|
|
.tier-FOUNDER { border-color: #9e6a03; color: #e3b341; }
|
|
.tier-DEMO { border-color: #388bfd; color: #79c0ff; }
|
|
|
|
.expired { color: #f85149; }
|
|
.ok { color: #3fb950; }
|
|
|
|
.pagination { padding: 1rem 1.5rem; display: flex; gap: 1rem; align-items: center; border-top: 1px solid #21262d; }
|
|
.page-link { font-size: 0.8rem; color: #58a6ff; }
|
|
.page-info { font-size: 0.75rem; color: #484f58; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<nav>
|
|
<div class="nav-logo">APIX · <span>Admin</span></div>
|
|
<a class="nav-link" href="/">Dashboard</a>
|
|
<a class="nav-link active" href="/sandboxes">Sandboxes</a>
|
|
<div class="nav-right"><a class="nav-link" href="/login/logout">Sign out</a></div>
|
|
</nav>
|
|
|
|
<div class="toolbar">
|
|
<span class="toolbar-label">Filter:</span>
|
|
<a class="filter-link {#if tier == ''}active{/if}" href="/sandboxes">All</a>
|
|
<a class="filter-link {#if tier == 'FREE'}active{/if}" href="/sandboxes?tier=FREE">FREE</a>
|
|
<a class="filter-link {#if tier == 'COMMUNITY'}active{/if}" href="/sandboxes?tier=COMMUNITY">COMMUNITY</a>
|
|
<a class="filter-link {#if tier == 'FOUNDER'}active{/if}" href="/sandboxes?tier=FOUNDER">FOUNDER</a>
|
|
<a class="filter-link {#if tier == 'DEMO'}active{/if}" href="/sandboxes?tier=DEMO">DEMO</a>
|
|
<span class="count">{result.total} total</span>
|
|
</div>
|
|
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>Name</th>
|
|
<th>Tier</th>
|
|
<th>Services</th>
|
|
<th>Requests</th>
|
|
<th>Location</th>
|
|
<th>Expires</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{#for sb in result.sandboxes}
|
|
<tr>
|
|
<td><a href="/sandbox/{sb.sandboxId}">{sb.name}</a>
|
|
<div style="font-size:0.65rem;color:#484f58;margin-top:2px">{sb.sandboxId}</div>
|
|
</td>
|
|
<td><span class="tier-badge tier-{sb.tier}">{sb.tier}</span></td>
|
|
<td>{sb.serviceCount}</td>
|
|
<td>{sb.totalRequests}</td>
|
|
<td>{#if sb.location != null}{sb.location}{#else}<span style="color:#484f58">—</span>{/if}</td>
|
|
<td class="{#if sb.expired}expired{#else}ok{/if}" data-expires="{sb.expiresAt}">{sb.expiresAt}</td>
|
|
</tr>
|
|
{/for}
|
|
</tbody>
|
|
</table>
|
|
|
|
<div class="pagination">
|
|
{#if page > 0}
|
|
<a class="page-link" href="/sandboxes?page={page - 1}&size={size}&tier={tier}">← Previous</a>
|
|
{/if}
|
|
<span class="page-info">Page {page + 1} · {result.sandboxes.size} of {result.total}</span>
|
|
{#if (page + 1) * size < result.total}
|
|
<a class="page-link" href="/sandboxes?page={page + 1}&size={size}&tier={tier}">Next →</a>
|
|
{/if}
|
|
</div>
|
|
|
|
<script>
|
|
document.querySelectorAll('[data-expires]').forEach(td => {
|
|
const d = new Date(td.dataset.expires);
|
|
const now = new Date();
|
|
const days = Math.round((d - now) / 86400000);
|
|
td.textContent = days < 0 ? ('expired ' + (-days) + 'd ago') : ('in ' + days + 'd');
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|