rtrm's picture
rtrm HF Staff
feat: redesign GPU capacity dashboard
ed8230e unverified
Raw
History Blame Contribute Delete
43.6 kB
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>AWS GPU Capacity</title>
<style>
@import url("https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&family=IBM+Plex+Sans:wght@400;500;600&display=swap");
:root {
color-scheme: light dark;
--page: #f3f5f2;
--surface: #ffffff;
--surface-subtle: #f7f8f6;
--surface-strong: #eef1ed;
--ink: #15211d;
--muted: #66736d;
--muted-strong: #48554f;
--line: #d9dfda;
--line-strong: #c4cdc6;
--accent: #214b42;
--accent-soft: #e8efec;
--ok: #14744a;
--ok-soft: #e9f4ed;
--ko: #b3453c;
--ko-soft: #f7ecea;
--warn: #966916;
--warn-soft: #f7f1e5;
--na: #7d8782;
--na-soft: #f0f2f0;
--row-hover: #fafbf9;
--shadow: 0 18px 42px rgba(21, 33, 29, 0.08);
--panel-shadow: -18px 0 42px rgba(15, 24, 20, 0.14);
}
@media (prefers-color-scheme: dark) {
:root {
--page: #111613;
--surface: #171d19;
--surface-subtle: #1c231f;
--surface-strong: #222b25;
--ink: #edf1ee;
--muted: #99a59e;
--muted-strong: #bdc8c1;
--line: #29322d;
--line-strong: #39453e;
--accent: #d6e5dd;
--accent-soft: #202c27;
--ok: #6fc48e;
--ok-soft: #18291f;
--ko: #e18b83;
--ko-soft: #2c201f;
--warn: #d9b468;
--warn-soft: #2d281d;
--na: #9ba59f;
--na-soft: #202622;
--row-hover: #1a211d;
--shadow: 0 20px 44px rgba(0, 0, 0, 0.24);
--panel-shadow: -20px 0 44px rgba(0, 0, 0, 0.34);
}
}
* {
box-sizing: border-box;
letter-spacing: 0;
}
html {
min-height: 100%;
}
body {
min-height: 100vh;
margin: 0;
background: var(--page);
color: var(--ink);
font: 14px/1.45 "IBM Plex Sans", "Avenir Next", sans-serif;
}
button,
input {
font: inherit;
}
a {
color: var(--accent);
text-decoration-color: var(--line-strong);
text-underline-offset: 2px;
}
a:hover {
text-decoration-color: currentColor;
}
button {
color: var(--ink);
}
button:focus-visible,
input:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
.app {
width: min(calc(100% - 48px), 1600px);
margin: 0 auto;
padding: 28px 0 36px;
}
.topbar {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 32px;
margin-bottom: 24px;
}
h1 {
margin: 0;
color: var(--ink);
font-size: 29px;
font-weight: 600;
line-height: 1.12;
}
.subtitle {
max-width: 640px;
margin: 8px 0 0;
color: var(--muted);
font-size: 14px;
}
#authline {
display: flex;
align-items: center;
justify-content: flex-end;
min-height: 36px;
color: var(--muted);
font-size: 13px;
text-align: right;
}
.session-pill {
display: inline-flex;
align-items: center;
min-height: 36px;
padding: 0 12px;
border: 1px solid var(--line);
border-radius: 6px;
background: var(--surface);
color: var(--muted-strong);
white-space: nowrap;
}
.summary-band {
display: grid;
grid-template-columns: repeat(5, minmax(0, 1fr));
margin-bottom: 18px;
overflow: hidden;
border: 1px solid var(--line);
border-radius: 8px;
background: var(--surface);
box-shadow: var(--shadow);
}
.summary-item {
min-width: 0;
padding: 15px 18px 14px;
border-right: 1px solid var(--line);
}
.summary-item:last-child {
border-right: 0;
}
.summary-label {
display: block;
margin-bottom: 5px;
color: var(--muted);
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
}
.summary-value {
display: block;
color: var(--ink);
font-size: 23px;
font-weight: 600;
line-height: 1.15;
}
.summary-value.ok {
color: var(--ok);
}
.summary-value.ko {
color: var(--ko);
}
.summary-value.warn {
color: var(--warn);
}
.summary-note {
display: block;
margin-top: 4px;
overflow: hidden;
color: var(--muted);
font-size: 12px;
text-overflow: ellipsis;
white-space: nowrap;
}
.toolbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
margin-bottom: 14px;
}
.toolbar-left,
.toolbar-right {
display: flex;
align-items: center;
gap: 10px;
}
.search-field {
display: flex;
align-items: center;
width: 246px;
height: 38px;
padding: 0 11px;
border: 1px solid var(--line);
border-radius: 6px;
background: var(--surface);
transition: border-color 0.15s ease;
}
.search-field:focus-within {
border-color: var(--line-strong);
}
.search-field span {
margin-right: 8px;
color: var(--muted);
font-size: 12px;
font-weight: 500;
}
.search-field input {
min-width: 0;
width: 100%;
border: 0;
background: transparent;
color: var(--ink);
outline: 0;
font-size: 13px;
}
.search-field input::placeholder {
color: var(--muted);
}
.toggle {
display: inline-flex;
align-items: center;
gap: 8px;
height: 38px;
padding: 0 11px;
border: 1px solid var(--line);
border-radius: 6px;
background: var(--surface);
color: var(--muted-strong);
cursor: pointer;
font-size: 13px;
white-space: nowrap;
}
.toggle input {
width: 14px;
height: 14px;
margin: 0;
accent-color: var(--accent);
}
#addform {
display: flex;
align-items: center;
gap: 8px;
}
#addtype {
width: 254px;
height: 38px;
padding: 0 12px;
border: 1px solid var(--line);
border-radius: 6px;
background: var(--surface);
color: var(--ink);
font-size: 13px;
}
#addtype::placeholder {
color: var(--muted);
}
.button {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 38px;
padding: 0 13px;
border: 1px solid var(--line-strong);
border-radius: 6px;
background: var(--surface);
color: var(--ink);
cursor: pointer;
font-size: 13px;
font-weight: 500;
transition: background 0.15s ease, border-color 0.15s ease;
}
.button:hover:not(:disabled) {
border-color: var(--accent);
background: var(--accent-soft);
}
.button.primary {
border-color: var(--accent);
background: var(--accent);
color: var(--surface);
}
.button.primary:hover:not(:disabled) {
background: var(--ink);
}
.button:disabled {
opacity: 0.55;
cursor: wait;
}
.matrix-shell {
overflow: hidden;
border: 1px solid var(--line);
border-radius: 8px;
background: var(--surface);
box-shadow: var(--shadow);
}
.matrix-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 24px;
padding: 18px 20px 16px;
border-bottom: 1px solid var(--line);
}
.matrix-title {
margin: 0;
font-size: 15px;
font-weight: 600;
}
.matrix-copy {
margin: 3px 0 0;
color: var(--muted);
font-size: 12px;
}
.legend {
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
gap: 8px 14px;
max-width: 660px;
color: var(--muted);
font-size: 12px;
}
.legend-item {
display: inline-flex;
align-items: center;
gap: 6px;
white-space: nowrap;
}
.signal {
display: inline-block;
flex: none;
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--na);
}
.signal.available {
background: var(--ok);
}
.signal.partial {
border: 1px solid var(--ok);
background: linear-gradient(90deg, var(--ok) 50%, transparent 50%);
}
.signal.unavailable {
background: var(--ko);
}
.signal.quota,
.signal.forbidden {
background: var(--warn);
}
.signal.unsupported,
.signal.not_offered,
.signal.error,
.signal.never {
background: var(--na);
}
.matrix-scroll {
min-height: 430px;
max-height: calc(100vh - 346px);
overflow: auto;
}
.matrix {
width: max-content;
min-width: 100%;
border-collapse: separate;
border-spacing: 0;
table-layout: fixed;
}
.matrix .type-col {
width: 248px;
}
.matrix .region-col {
width: 116px;
}
.matrix .coverage-col {
width: 106px;
}
.matrix .action-col {
width: 84px;
}
.matrix th,
.matrix td {
border-bottom: 1px solid var(--line);
}
.matrix thead th {
background: var(--surface-subtle);
color: var(--muted-strong);
font-weight: 500;
}
.scope-row th {
position: sticky;
top: 0;
z-index: 5;
height: 34px;
padding: 0 12px;
border-bottom: 1px solid var(--line);
color: var(--muted);
font-size: 11px;
font-weight: 600;
text-align: left;
text-transform: uppercase;
}
.scope-row .region-group-head {
border-left: 1px solid var(--line);
}
.scope-row .coverage-head {
border-left: 1px solid var(--line);
}
.region-row th {
position: sticky;
top: 34px;
z-index: 5;
height: 60px;
padding: 9px 10px 8px;
border-left: 1px solid var(--line);
text-align: left;
}
.type-head {
position: sticky !important;
left: 0;
z-index: 7 !important;
padding-left: 20px !important;
vertical-align: bottom;
}
.coverage-head,
.action-head {
vertical-align: bottom;
}
.region-name {
display: block;
color: var(--ink);
font: 500 12px/1.2 "IBM Plex Mono", monospace;
white-space: nowrap;
}
.region-summary {
display: block;
margin-top: 5px;
color: var(--muted);
font-size: 11px;
white-space: nowrap;
}
.region-summary strong {
color: var(--muted-strong);
font-weight: 500;
}
.family-row th {
height: 34px;
padding: 0 20px;
background: var(--surface-strong);
color: var(--muted-strong);
font-size: 11px;
font-weight: 600;
text-align: left;
text-transform: uppercase;
}
.family-count {
margin-left: 8px;
color: var(--muted);
font-weight: 500;
text-transform: none;
}
.data-row th,
.data-row td {
height: 62px;
background: var(--surface);
transition: background 0.15s ease;
}
.data-row:hover th,
.data-row:hover td {
background: var(--row-hover);
}
.type-cell {
position: sticky;
left: 0;
z-index: 2;
padding: 9px 14px 9px 20px;
border-right: 1px solid var(--line);
text-align: left;
}
.type-name {
display: block;
color: var(--ink);
font: 500 13px/1.25 "IBM Plex Mono", monospace;
white-space: nowrap;
}
.gpu-meta {
display: block;
max-width: 205px;
margin-top: 3px;
overflow: hidden;
color: var(--muted);
font-size: 11px;
font-weight: 400;
text-overflow: ellipsis;
white-space: nowrap;
}
.matrix-cell {
padding: 0;
border-left: 1px solid var(--line);
}
.cell-button {
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: center;
gap: 3px;
width: 100%;
height: 61px;
padding: 8px 10px;
border: 0;
background: transparent;
cursor: pointer;
text-align: left;
}
.cell-button:hover {
background: var(--surface-subtle);
}
.matrix-cell.available .cell-button,
.matrix-cell.partial .cell-button {
background: var(--ok-soft);
}
.matrix-cell.unavailable .cell-button {
background: var(--ko-soft);
}
.matrix-cell.quota .cell-button,
.matrix-cell.forbidden .cell-button {
background: var(--warn-soft);
}
.matrix-cell.unsupported .cell-button,
.matrix-cell.not_offered .cell-button,
.matrix-cell.error .cell-button {
background: var(--na-soft);
}
.cell-main {
display: flex;
align-items: center;
gap: 6px;
min-width: 0;
}
.cell-value {
color: var(--muted-strong);
font: 500 13px/1.1 "IBM Plex Mono", monospace;
white-space: nowrap;
}
.matrix-cell.available .cell-value,
.matrix-cell.partial .cell-value {
color: var(--ok);
}
.matrix-cell.unavailable .cell-value {
color: var(--ko);
}
.matrix-cell.quota .cell-value,
.matrix-cell.forbidden .cell-value {
color: var(--warn);
}
.matrix-cell.unsupported .cell-value,
.matrix-cell.not_offered .cell-value,
.matrix-cell.error .cell-value,
.matrix-cell.never .cell-value {
color: var(--na);
}
.cell-age {
color: var(--muted);
font-size: 11px;
white-space: nowrap;
}
.matrix-cell.stale .cell-main,
.matrix-cell.stale .cell-age {
opacity: 0.5;
}
.matrix-cell.probing .cell-button {
cursor: wait;
}
.matrix-cell.probing .cell-main,
.matrix-cell.probing .cell-age {
animation: pulse 1s ease-in-out infinite;
}
@keyframes pulse {
50% {
opacity: 0.35;
}
}
.coverage-cell {
padding: 0 12px;
border-left: 1px solid var(--line);
}
.coverage-value {
display: block;
color: var(--ink);
font: 500 13px/1.2 "IBM Plex Mono", monospace;
white-space: nowrap;
}
.coverage-label {
display: block;
margin-top: 3px;
color: var(--muted);
font-size: 11px;
white-space: nowrap;
}
.action-cell {
padding: 0 10px;
text-align: right;
}
.row-probe {
height: 30px;
padding: 0 10px;
border: 1px solid var(--line);
border-radius: 5px;
background: transparent;
color: var(--muted-strong);
cursor: pointer;
font-size: 12px;
font-weight: 500;
}
.row-probe:hover:not(:disabled) {
border-color: var(--accent);
color: var(--ink);
}
.row-probe:disabled {
opacity: 0.45;
cursor: wait;
}
.empty-filter {
display: none;
padding: 64px 20px;
color: var(--muted);
text-align: center;
}
.empty-filter.visible {
display: block;
}
#backdrop {
position: fixed;
inset: 0;
z-index: 9;
display: none;
background: rgba(14, 20, 17, 0.24);
}
#backdrop.open {
display: block;
}
#panel {
position: fixed;
top: 0;
right: 0;
z-index: 10;
width: 438px;
max-width: 94vw;
height: 100vh;
overflow-y: auto;
border-left: 1px solid var(--line);
background: var(--surface);
box-shadow: var(--panel-shadow);
transform: translateX(102%);
transition: transform 0.2s ease-out;
}
#panel.open {
transform: translateX(0);
}
.panel-inner {
padding: 25px 24px 28px;
}
.panel-head {
position: relative;
padding-right: 42px;
}
.panel-kicker {
margin-bottom: 7px;
color: var(--muted);
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
}
#panel h2 {
margin: 0;
color: var(--ink);
font: 500 18px/1.25 "IBM Plex Mono", monospace;
}
#p-gpu {
margin-top: 5px;
color: var(--muted);
font-size: 12px;
}
#panel .close {
position: absolute;
top: -4px;
right: 0;
display: flex;
align-items: center;
justify-content: center;
width: 34px;
height: 34px;
border: 1px solid var(--line);
border-radius: 6px;
background: transparent;
color: var(--muted-strong);
cursor: pointer;
font-size: 20px;
line-height: 1;
}
#panel .close:hover {
border-color: var(--accent);
color: var(--ink);
}
.panel-section {
margin-top: 28px;
}
.section-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-bottom: 12px;
}
.section-label {
color: var(--muted);
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
}
#p-age {
color: var(--muted);
font-size: 12px;
}
#p-summary {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
min-height: 48px;
margin-bottom: 14px;
padding: 0 14px;
border: 1px solid var(--line);
border-radius: 6px;
background: var(--surface-subtle);
}
.panel-status {
display: inline-flex;
align-items: center;
gap: 8px;
color: var(--muted-strong);
font-weight: 500;
}
.panel-count {
color: var(--muted);
font: 500 12px/1.2 "IBM Plex Mono", monospace;
white-space: nowrap;
}
#p-azs {
border-top: 1px solid var(--line);
}
.azrow {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
min-height: 38px;
padding: 10px 0;
border-bottom: 1px solid var(--line);
font-size: 13px;
}
.az-name {
color: var(--ink);
font: 500 12px/1.35 "IBM Plex Mono", monospace;
white-space: nowrap;
}
.az-detail {
min-width: 0;
color: var(--muted-strong);
text-align: right;
}
.az-status {
display: inline-flex;
align-items: center;
gap: 6px;
}
.az-error {
display: block;
margin-top: 2px;
overflow: hidden;
color: var(--muted);
font: 400 11px/1.3 "IBM Plex Mono", monospace;
text-overflow: ellipsis;
white-space: nowrap;
}
#p-probe {
width: 100%;
margin-top: 16px;
}
.ranges {
display: flex;
gap: 6px;
}
.ranges button {
height: 28px;
padding: 0 9px;
border: 1px solid var(--line);
border-radius: 5px;
background: transparent;
color: var(--muted-strong);
cursor: pointer;
font-size: 12px;
}
.ranges button.active {
border-color: var(--accent);
background: var(--accent-soft);
color: var(--ink);
}
#chartwrap {
position: relative;
min-height: 170px;
padding-top: 6px;
}
#chartwrap svg {
display: block;
width: 100%;
height: auto;
}
#tooltip {
position: absolute;
z-index: 11;
display: none;
padding: 6px 8px;
border: 1px solid var(--line);
border-radius: 5px;
background: var(--surface);
box-shadow: 0 8px 20px rgba(15, 24, 20, 0.12);
color: var(--muted-strong);
font-size: 11px;
pointer-events: none;
white-space: nowrap;
}
.empty {
padding: 20px 0;
color: var(--muted);
font-size: 13px;
}
@media (max-width: 1120px) {
.summary-band {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.summary-item:nth-child(3) {
border-right: 0;
}
.summary-item:nth-child(n + 4) {
border-top: 1px solid var(--line);
}
.toolbar {
align-items: flex-start;
flex-direction: column;
}
.toolbar-right {
width: 100%;
}
#addform {
width: 100%;
}
#addtype {
flex: 1;
width: auto;
}
.matrix-scroll {
max-height: none;
min-height: 0;
}
}
@media (max-width: 720px) {
.app {
width: min(calc(100% - 28px), 1600px);
padding-top: 18px;
}
.topbar {
flex-direction: column;
gap: 14px;
margin-bottom: 18px;
}
h1 {
font-size: 24px;
}
#authline {
justify-content: flex-start;
text-align: left;
}
.summary-band {
grid-template-columns: 1fr 1fr;
}
.summary-item {
border-right: 1px solid var(--line);
border-top: 0;
}
.summary-item:nth-child(even) {
border-right: 0;
}
.summary-item:nth-child(n + 3) {
border-top: 1px solid var(--line);
}
.summary-item:last-child {
grid-column: 1 / -1;
border-right: 0;
}
.toolbar-left {
align-items: stretch;
flex-direction: column;
width: 100%;
}
.search-field,
.toggle {
width: 100%;
}
.matrix-header {
flex-direction: column;
gap: 12px;
padding: 16px 14px 14px;
}
.legend {
justify-content: flex-start;
}
.type-head {
padding-left: 14px !important;
}
.type-cell,
.family-row th {
padding-left: 14px;
}
#panel {
width: 100%;
max-width: 100%;
}
}
</style>
</head>
<body>
<main class="app">
<header class="topbar">
<div>
<h1>AWS GPU Capacity</h1>
<p class="subtitle">Near real-time GPU availability by instance type and region.</p>
</div>
<div id="authline"></div>
</header>
<section class="summary-band" id="summary" aria-label="Capacity summary"></section>
<div class="toolbar">
<div class="toolbar-left">
<label class="search-field">
<span>Filter</span>
<input id="filter" type="search" placeholder="g5, p5, A10G...">
</label>
<label class="toggle">
<input id="available-only" type="checkbox">
<span>Available only</span>
</label>
</div>
<div class="toolbar-right">
<form id="addform">
<input id="addtype" placeholder="Probe another size, e.g. g5.12xlarge">
<button class="button primary" type="submit">Probe</button>
</form>
</div>
</div>
<section class="matrix-shell" aria-labelledby="matrix-title">
<div class="matrix-header">
<div>
<h2 class="matrix-title" id="matrix-title">Capacity matrix</h2>
<p class="matrix-copy" id="matrix-copy"></p>
</div>
<div class="legend" aria-label="Status legend">
<span class="legend-item"><span class="signal available"></span>Available</span>
<span class="legend-item"><span class="signal partial"></span>Partial AZ coverage</span>
<span class="legend-item"><span class="signal unavailable"></span>No capacity</span>
<span class="legend-item"><span class="signal quota"></span>Quota or denied</span>
<span class="legend-item"><span class="signal unsupported"></span>N.O. / n/s / stale</span>
</div>
</div>
<div class="matrix-scroll">
<table class="matrix" id="grid"></table>
<div class="empty-filter" id="empty-filter">No instance type matches the current filters.</div>
</div>
</section>
</main>
<div id="backdrop"></div>
<aside id="panel" aria-hidden="true">
<div class="panel-inner">
<header class="panel-head">
<div class="panel-kicker">Region detail</div>
<h2 id="p-title"></h2>
<div id="p-gpu"></div>
<button class="close" type="button" title="Close" aria-label="Close">&times;</button>
</header>
<section class="panel-section">
<div class="section-head">
<div class="section-label">Current status</div>
<div id="p-age"></div>
</div>
<div id="p-summary"></div>
<div id="p-azs"></div>
<button class="button primary" id="p-probe" type="button">Probe region now</button>
</section>
<section class="panel-section">
<div class="section-head">
<div class="section-label">Available AZs over time</div>
<div class="ranges" id="p-ranges">
<button type="button" data-hours="24">24h</button>
<button type="button" data-hours="48" class="active">48h</button>
<button type="button" data-hours="168">7d</button>
</div>
</div>
<div id="chartwrap"><div id="tooltip"></div></div>
</section>
</div>
</aside>
<script>
let META = null;
let ME = { can_probe: false, authenticated: false };
let PANEL = { type: null, region: null, hours: 48 };
let VIEW = { query: "", availableOnly: false };
const PROBING = new Set();
const STATUS_COLOR = {
available: "var(--ok)",
partial: "var(--ok)",
unavailable: "var(--ko)",
quota: "var(--warn)",
forbidden: "var(--warn)",
};
function esc(value) {
return String(value ?? "")
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
}
function fmtAge(ts) {
if (!ts) return "never";
const s = Math.max(0, Math.round(Date.now() / 1000 - ts));
if (s < 90) return s + "s ago";
if (s < 5400) return Math.round(s / 60) + "m ago";
if (s < 172800) return Math.round(s / 3600) + "h ago";
return Math.round(s / 86400) + "d ago";
}
function isStale(result) {
return Boolean(result && result.checked_at && Date.now() / 1000 - result.checked_at > META.stale_after_seconds);
}
function azCounts(result) {
const azs = Object.entries((result && result.azs) || {});
return {
azs,
available: azs.filter(([, az]) => az.status === "available").length,
total: azs.length,
};
}
function humanStatus(status) {
return {
available: "Available",
partial: "Partial availability",
unavailable: "No capacity",
quota: "Quota blocked",
forbidden: "Access denied",
unsupported: "No ODCR support",
not_offered: "Not offered",
error: "Probe error",
never: "Never probed",
}[status] || status;
}
function cellModel(result) {
if (!result) {
return {
cls: "never",
status: "never",
label: "-",
age: "Never probed",
title: "Never probed",
detail: "Never probed",
count: "",
};
}
const counts = azCounts(result);
const status = result.status === "available" && counts.available < counts.total ? "partial" : result.status;
const label = {
available: `${counts.available}/${counts.total}`,
partial: `${counts.available}/${counts.total}`,
unavailable: counts.total ? `0/${counts.total}` : "none",
quota: "quota",
forbidden: "denied",
unsupported: "n/s",
not_offered: "N.O.",
error: "error",
}[status] || status;
const title = counts.azs.map(([az, value]) =>
`${az}: ${value.status}${value.error ? " (" + value.error + ")" : ""}`
).join("\n") || (result.status === "not_offered" ? "Not offered in this region" : result.error || result.status);
const count = counts.total ? `${counts.available}/${counts.total} AZ` : "";
return {
cls: status + (isStale(result) ? " stale" : ""),
status,
label,
age: fmtAge(result.checked_at),
title,
detail: humanStatus(status),
count,
};
}
function familyLabel(instanceType) {
const base = instanceType.split(".")[0];
const match = base.match(/^([a-z]+)(\d+)/);
if (!match) return base.toUpperCase();
if (match[1] === "g") return "G" + match[2];
if (match[1] === "gr") return "G" + match[2];
if (match[1] === "p") return "P series";
return base.toUpperCase();
}
function regionGroups(regions) {
const defs = [
{ label: "United States", test: region => region.startsWith("us-") },
{ label: "Europe", test: region => region.startsWith("eu-") },
{ label: "Other", test: () => true },
];
const remaining = new Set(regions);
return defs.map(def => {
const items = regions.filter(region => remaining.has(region) && def.test(region));
items.forEach(region => remaining.delete(region));
return { label: def.label, regions: items };
}).filter(group => group.regions.length);
}
function filteredTypes(data) {
const query = VIEW.query.trim().toLowerCase();
return data.instance_types.filter(type => {
const gpu = (data.gpu_info || {})[type];
const haystack = `${type} ${gpu ? `${gpu.name} ${gpu.count} ${gpu.memory_gb}` : ""}`.toLowerCase();
if (query && !haystack.includes(query)) return false;
if (VIEW.availableOnly) {
return data.regions.some(region => ((data.state[type] || {})[region] || {}).status === "available");
}
return true;
});
}
function groupedTypes(types) {
const groups = [];
types.forEach(type => {
const label = familyLabel(type);
let group = groups.find(item => item.label === label);
if (!group) {
group = { label, types: [] };
groups.push(group);
}
group.types.push(type);
});
return groups;
}
function probeKey(type, region) {
return `${type}::${region || "*"}`;
}
function isProbing(type, region) {
return PROBING.has(probeKey(type, region)) || PROBING.has(probeKey(type, null));
}
function renderSummary(data) {
const results = [];
data.instance_types.forEach(type => {
data.regions.forEach(region => {
const result = (data.state[type] || {})[region];
if (result) results.push(result);
});
});
const available = results.filter(result => result.status === "available").length;
const partial = results.filter(result => {
const counts = azCounts(result);
return result.status === "available" && counts.available < counts.total;
}).length;
const unavailable = results.filter(result => result.status === "unavailable").length;
const stale = results.filter(result => isStale(result)).length;
const newest = Math.max(0, ...results.map(result => result.checked_at || 0));
const total = data.instance_types.length * data.regions.length;
const metrics = [
{ label: "Available signals", value: available, note: "type-region pairs", tone: "ok" },
{ label: "Partial coverage", value: partial, note: "usable in some AZs", tone: partial ? "warn" : "" },
{ label: "No capacity", value: unavailable, note: "probed cells", tone: unavailable ? "ko" : "" },
{ label: "Stale results", value: stale, note: `older than ${Math.round(data.stale_after_seconds / 60)}m`, tone: stale ? "warn" : "" },
{ label: "Latest probe", value: newest ? fmtAge(newest) : "-", note: `${results.length}/${total} cells covered`, tone: "" },
];
document.getElementById("summary").innerHTML = metrics.map(metric => `
<div class="summary-item">
<span class="summary-label">${esc(metric.label)}</span>
<span class="summary-value ${metric.tone}">${esc(metric.value)}</span>
<span class="summary-note">${esc(metric.note)}</span>
</div>
`).join("");
}
function regionSummary(data, region, types) {
const results = types.map(type => (data.state[type] || {})[region]).filter(Boolean);
return {
available: results.filter(result => result.status === "available").length,
known: results.length,
};
}
function coverageSummary(data, type) {
const results = data.regions.map(region => (data.state[type] || {})[region]).filter(Boolean);
return {
available: results.filter(result => result.status === "available").length,
known: results.length,
};
}
function renderMatrix(data) {
const visibleTypes = filteredTypes(data);
const groups = regionGroups(data.regions);
const displayRegions = groups.flatMap(group => group.regions);
const typeGroups = groupedTypes(visibleTypes);
const table = document.getElementById("grid");
const empty = document.getElementById("empty-filter");
const copy = document.getElementById("matrix-copy");
copy.textContent = `${visibleTypes.length} of ${data.instance_types.length} instance types across ${data.regions.length} regions`;
const colgroup = `
<colgroup>
<col class="type-col">
${displayRegions.map(() => '<col class="region-col">').join("")}
<col class="coverage-col">
<col class="action-col">
</colgroup>
`;
const header = `
<thead>
<tr class="scope-row">
<th class="type-head" scope="col" rowspan="2">Instance type</th>
${groups.map(group => `<th class="region-group-head" scope="colgroup" colspan="${group.regions.length}">${esc(group.label)}</th>`).join("")}
<th class="coverage-head" scope="col" rowspan="2">Regions</th>
<th class="action-head" scope="col" rowspan="2"></th>
</tr>
<tr class="region-row">
${displayRegions.map(region => {
const summary = regionSummary(data, region, visibleTypes);
return `<th scope="col">
<span class="region-name">${esc(region)}</span>
<span class="region-summary"><strong>${summary.available}</strong> avail${summary.known ? ` / ${summary.known}` : ""}</span>
</th>`;
}).join("")}
</tr>
</thead>
`;
const body = typeGroups.map(group => `
<tr class="family-row">
<th scope="rowgroup" colspan="${displayRegions.length + 3}">
${esc(group.label)}<span class="family-count">${group.types.length} type${group.types.length === 1 ? "" : "s"}</span>
</th>
</tr>
${group.types.map(type => {
const gpu = (data.gpu_info || {})[type];
const coverage = coverageSummary(data, type);
const busy = isProbing(type, null);
return `<tr class="data-row">
<th class="type-cell" scope="row">
<span class="type-name">${esc(type)}</span>
<span class="gpu-meta">${gpu ? `${esc(gpu.count)}x ${esc(gpu.name)} ${esc(gpu.memory_gb)} GB` : "GPU metadata unavailable"}</span>
</th>
${displayRegions.map(region => {
const result = (data.state[type] || {})[region];
const model = cellModel(result);
const probing = isProbing(type, region);
const aria = `${type} in ${region}: ${model.detail}${model.count ? `, ${model.count}` : ""}, ${model.age}`;
return `<td class="matrix-cell ${esc(model.cls)}${probing ? " probing" : ""}" data-type="${esc(type)}" data-region="${esc(region)}">
<button class="cell-button" type="button" data-type="${esc(type)}" data-region="${esc(region)}" title="${esc(model.title)}" aria-label="${esc(aria)}">
<span class="cell-main"><span class="signal ${esc(model.status)}"></span><span class="cell-value">${esc(model.label)}</span></span>
<span class="cell-age">${esc(model.age)}</span>
</button>
</td>`;
}).join("")}
<td class="coverage-cell">
<span class="coverage-value">${coverage.available}/${data.regions.length}</span>
<span class="coverage-label">${coverage.known ? `${coverage.known} checked` : "not checked"}</span>
</td>
<td class="action-cell">
${ME.can_probe ? `<button class="row-probe" type="button" data-type="${esc(type)}" ${busy ? "disabled" : ""}>Probe</button>` : ""}
</td>
</tr>`;
}).join("")}
`).join("");
table.innerHTML = colgroup + header + `<tbody>${body}</tbody>`;
empty.classList.toggle("visible", visibleTypes.length === 0);
table.style.display = visibleTypes.length ? "" : "none";
}
function render(data) {
META = data;
renderSummary(data);
renderMatrix(data);
document.getElementById("addform").style.display = ME.can_probe ? "" : "none";
}
async function refresh() {
const response = await fetch("/api/state");
render(await response.json());
}
async function probe(type, region) {
const key = probeKey(type, region);
if (PROBING.has(key)) return;
PROBING.add(key);
if (META) renderMatrix(META);
try {
await fetch("/api/probe", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(region ? { instance_type: type, region } : { instance_type: type }),
});
} finally {
PROBING.delete(key);
await refresh();
if (PANEL.type === type && document.getElementById("panel").classList.contains("open")) {
renderPanelStatus();
loadChart();
}
}
}
async function openPanel(type, region) {
PANEL.type = type;
PANEL.region = region;
document.getElementById("p-title").textContent = `${type} / ${region}`;
const gpu = (META.gpu_info || {})[type];
document.getElementById("p-gpu").textContent = gpu ? `${gpu.count}x ${gpu.name} ${gpu.memory_gb} GB` : "";
renderPanelStatus();
document.getElementById("p-probe").style.display = ME.can_probe ? "" : "none";
document.getElementById("panel").classList.add("open");
document.getElementById("panel").setAttribute("aria-hidden", "false");
document.getElementById("backdrop").classList.add("open");
await loadChart();
}
function renderPanelStatus() {
const result = (META.state[PANEL.type] || {})[PANEL.region];
const age = document.getElementById("p-age");
const summary = document.getElementById("p-summary");
const box = document.getElementById("p-azs");
if (!result) {
age.textContent = "";
summary.innerHTML = `<span class="panel-status"><span class="signal never"></span>Never probed</span>`;
box.innerHTML = '<div class="empty">No current result for this region.</div>';
return;
}
const model = cellModel(result);
const counts = azCounts(result);
age.textContent = fmtAge(result.checked_at);
summary.innerHTML = `
<span class="panel-status"><span class="signal ${esc(model.status)}"></span>${esc(model.detail)}</span>
<span class="panel-count">${esc(model.count || "")}</span>
`;
box.innerHTML = counts.azs.length
? counts.azs.map(([az, value]) => `
<div class="azrow">
<span class="az-name">${esc(az)}</span>
<span class="az-detail">
<span class="az-status"><span class="signal ${esc(value.status)}"></span>${esc(humanStatus(value.status))}</span>
${value.error ? `<span class="az-error">${esc(value.error)}</span>` : ""}
</span>
</div>
`).join("")
: `<div class="empty">${result.status === "not_offered" ? "Not offered in this region." : esc(humanStatus(result.status)) + "."}</div>`;
}
async function loadChart() {
const wrap = document.getElementById("chartwrap");
[...wrap.querySelectorAll("svg, .empty")].forEach(node => node.remove());
const response = await fetch(`/api/history/${encodeURIComponent(PANEL.type)}?region=${encodeURIComponent(PANEL.region)}&hours=${PANEL.hours}`);
const series = (await response.json()).series;
if (!series.length) {
wrap.insertAdjacentHTML("beforeend", '<div class="empty">No history yet for this cell.</div>');
return;
}
drawChart(wrap, series);
}
function drawChart(wrap, series) {
const W = 390, H = 168, L = 28, R = 8, T = 10, B = 24;
const now = Date.now() / 1000;
const t0 = now - PANEL.hours * 3600;
const yMax = Math.max(1, ...series.map(point => point.total));
const x = time => L + (time - t0) / (now - t0) * (W - L - R);
const y = value => T + (1 - value / yMax) * (H - T - B);
let gridLines = "";
let yLabels = "";
const yStep = yMax > 8 ? 2 : 1;
for (let value = 0; value <= yMax; value += yStep) {
gridLines += `<line x1="${L}" y1="${y(value)}" x2="${W - R}" y2="${y(value)}" stroke="var(--line)" stroke-width="1"/>`;
yLabels += `<text x="${L - 6}" y="${y(value) + 3}" text-anchor="end" font-size="9" fill="var(--muted)">${value}</text>`;
}
let xLabels = "";
for (let index = 0; index <= 3; index++) {
const time = t0 + (now - t0) * index / 3;
const date = new Date(time * 1000);
const label = PANEL.hours <= 48
? date.toTimeString().slice(0, 5)
: `${date.getDate()}/${date.getMonth() + 1} ${date.getHours()}h`;
const anchor = index === 0 ? "start" : index === 3 ? "end" : "middle";
xLabels += `<text x="${x(time)}" y="${H - 6}" text-anchor="${anchor}" font-size="9" fill="var(--muted)">${label}</text>`;
}
const gap = Math.max(45 * 60, (now - t0) / 30);
let path = "";
series.forEach((point, index) => {
const px = x(point.t);
const py = y(point.available);
if (index === 0 || point.t - series[index - 1].t > gap) path += `M${px.toFixed(1)},${py.toFixed(1)}`;
else path += `H${px.toFixed(1)}V${py.toFixed(1)}`;
});
const dots = series.map((point, index) =>
`<circle cx="${x(point.t).toFixed(1)}" cy="${y(point.available).toFixed(1)}" r="3.5" data-i="${index}"
fill="${STATUS_COLOR[point.status] || "var(--na)"}" stroke="var(--surface)" stroke-width="2"/>`
).join("");
wrap.insertAdjacentHTML("beforeend", `
<svg viewBox="0 0 ${W} ${H}" role="img" aria-label="Available AZs over time">
${gridLines}${yLabels}${xLabels}
<path d="${path}" fill="none" stroke="var(--muted-strong)" stroke-width="2"/>
${dots}
</svg>
`);
const svg = wrap.querySelector("svg");
const tooltip = document.getElementById("tooltip");
svg.addEventListener("mousemove", event => {
const rect = svg.getBoundingClientRect();
const mouseX = (event.clientX - rect.left) / rect.width * W;
let best = null;
let distance = Infinity;
series.forEach((point, index) => {
const currentDistance = Math.abs(x(point.t) - mouseX);
if (currentDistance < distance) {
distance = currentDistance;
best = index;
}
});
if (best === null) return;
const point = series[best];
const date = new Date(point.t * 1000);
tooltip.innerHTML = `${esc(date.toLocaleString([], { day: "numeric", month: "short", hour: "2-digit", minute: "2-digit" }))} &middot; <b>${esc(point.available)}/${esc(point.total)} AZ</b> &middot; ${esc(humanStatus(point.status))}`;
tooltip.style.display = "block";
const tooltipX = x(point.t) / W * rect.width;
tooltip.style.left = Math.min(tooltipX + 8, rect.width - tooltip.offsetWidth - 4) + "px";
tooltip.style.top = (y(point.available) / H * rect.height - 34) + "px";
svg.querySelectorAll("circle").forEach(circle => circle.setAttribute("r", circle.dataset.i == best ? 5 : 3.5));
});
svg.addEventListener("mouseleave", () => {
tooltip.style.display = "none";
svg.querySelectorAll("circle").forEach(circle => circle.setAttribute("r", 3.5));
});
}
function closePanel() {
document.getElementById("panel").classList.remove("open");
document.getElementById("panel").setAttribute("aria-hidden", "true");
document.getElementById("backdrop").classList.remove("open");
PANEL.type = null;
PANEL.region = null;
}
document.getElementById("grid").addEventListener("click", event => {
const cell = event.target.closest(".cell-button");
if (cell) return openPanel(cell.dataset.type, cell.dataset.region);
const button = event.target.closest(".row-probe");
if (button) return probe(button.dataset.type, null);
});
document.getElementById("filter").addEventListener("input", event => {
VIEW.query = event.target.value;
if (META) renderMatrix(META);
});
document.getElementById("available-only").addEventListener("change", event => {
VIEW.availableOnly = event.target.checked;
if (META) renderMatrix(META);
});
document.getElementById("addform").addEventListener("submit", event => {
event.preventDefault();
const type = document.getElementById("addtype").value.trim().toLowerCase();
if (type) probe(type, null);
});
document.querySelector("#panel .close").addEventListener("click", closePanel);
document.getElementById("backdrop").addEventListener("click", closePanel);
document.addEventListener("keydown", event => {
if (event.key === "Escape") closePanel();
});
document.getElementById("p-probe").addEventListener("click", async event => {
event.target.disabled = true;
try {
await probe(PANEL.type, PANEL.region);
} finally {
event.target.disabled = false;
}
});
document.getElementById("p-ranges").addEventListener("click", event => {
const button = event.target.closest("button[data-hours]");
if (!button) return;
PANEL.hours = +button.dataset.hours;
document.querySelectorAll("#p-ranges button").forEach(item => item.classList.toggle("active", item === button));
loadChart();
});
async function loadMe() {
ME = await (await fetch("/api/me")).json();
const line = document.getElementById("authline");
if (ME.can_probe) {
line.innerHTML = ME.username
? `<span class="session-pill">Signed in as <b>&nbsp;${esc(ME.username)}</b>&nbsp;&middot;&nbsp;<a href="/logout">logout</a></span>`
: '<span class="session-pill">Probe access enabled</span>';
} else if (ME.authenticated) {
line.innerHTML = '<span class="session-pill">Read-only access</span>';
} else {
line.innerHTML = '<span class="session-pill">Read-only&nbsp;&middot;&nbsp;<a href="/login/huggingface" target="_blank">Sign in with HF</a></span>';
}
}
loadMe().then(refresh);
setInterval(refresh, 30000);
</script>
</body>
</html>