feat: refresh capacity every five minutes and highlight recent availability

#2
by GuillaumeSalouHF HF Staff - opened
Dockerfile CHANGED
@@ -10,7 +10,7 @@ USER user
10
 
11
  ENV HOST=0.0.0.0 \
12
  PORT=7860 \
13
- AUTO_REFRESH_MINUTES=15 \
14
  STATE_FILE=/data/state.json
15
 
16
  EXPOSE 7860
 
10
 
11
  ENV HOST=0.0.0.0 \
12
  PORT=7860 \
13
+ AUTO_REFRESH_MINUTES=5 \
14
  STATE_FILE=/data/state.json
15
 
16
  EXPOSE 7860
README.md CHANGED
@@ -28,7 +28,7 @@ AWS_PROFILE=<profile> uv run gpu-capacity
28
  ```
29
 
30
  One representative (smallest) size per GPU family, plus `g7e.48xlarge`, is swept
31
- automatically when `AUTO_REFRESH_MINUTES` is set (the deployed Space uses 15);
32
  any other size can be probed on demand from the dashboard input or the API.
33
  Results are cached in `state.json` and shown with their age; they fade once older than
34
  `STALE_AFTER_SECONDS` (default 900).
@@ -81,6 +81,7 @@ the Space).
81
  |---|---|
82
  | `available` (n/m AZ) | reservation succeeded in n of m offered AZs |
83
  | `no capacity` | AWS returned `InsufficientInstanceCapacity` everywhere |
 
84
  | `quota` / `denied` | account quota or IAM/SCP blocks the check (not a capacity signal) |
85
  | `n/s` | type not reservable via ODCR there (p5 → Capacity Blocks) |
86
  | `—` | type not offered in that region |
@@ -88,6 +89,11 @@ the Space).
88
  Note: results are account-specific (quotas, SCPs), which is what you want —
89
  "can *this* account get a G5 there".
90
 
 
 
 
 
 
91
  Latency: an `available` answer comes back in ~1 s per region; a `no capacity`
92
  answer takes 10–20 s because EC2 genuinely attempts placement before returning
93
  `InsufficientInstanceCapacity`. Probe creates run with retries disabled —
 
28
  ```
29
 
30
  One representative (smallest) size per GPU family, plus `g7e.48xlarge`, is swept
31
+ automatically when `AUTO_REFRESH_MINUTES` is set (the deployed Space uses 5);
32
  any other size can be probed on demand from the dashboard input or the API.
33
  Results are cached in `state.json` and shown with their age; they fade once older than
34
  `STALE_AFTER_SECONDS` (default 900).
 
81
  |---|---|
82
  | `available` (n/m AZ) | reservation succeeded in n of m offered AZs |
83
  | `no capacity` | AWS returned `InsufficientInstanceCapacity` everywhere |
84
+ | blue / `Seen <date>` | no capacity now, but at least one AZ had capacity within the past 10 days; the date is the latest positive observation |
85
  | `quota` / `denied` | account quota or IAM/SCP blocks the check (not a capacity signal) |
86
  | `n/s` | type not reservable via ODCR there (p5 → Capacity Blocks) |
87
  | `—` | type not offered in that region |
 
89
  Note: results are account-specific (quotas, SCPs), which is what you want —
90
  "can *this* account get a G5 there".
91
 
92
+ Blue is a historical hint, not current availability. Current availability stays
93
+ green and quota/access failures stay orange. Open a cell to see the last positive
94
+ observation and select the 10-day history range. This uses recorded observations
95
+ and does not trigger additional probes.
96
+
97
  Latency: an `available` answer comes back in ~1 s per region; a `no capacity`
98
  answer takes 10–20 s because EC2 genuinely attempts placement before returning
99
  `InsufficientInstanceCapacity`. Probe creates run with retries disabled —
gpu_capacity/config.py CHANGED
@@ -45,6 +45,9 @@ PROBE_END_DATE_MINUTES = 30
45
  # Cached probe results older than this are flagged stale in the UI.
46
  STALE_AFTER_SECONDS = int(os.environ.get("STALE_AFTER_SECONDS", "900"))
47
 
 
 
 
48
  # Optional background refresh of all types/regions. 0 = disabled (probe on
49
  # demand only). Each full sweep creates+cancels one reservation per offered AZ.
50
  AUTO_REFRESH_MINUTES = int(os.environ.get("AUTO_REFRESH_MINUTES", "0"))
 
45
  # Cached probe results older than this are flagged stale in the UI.
46
  STALE_AFTER_SECONDS = int(os.environ.get("STALE_AFTER_SECONDS", "900"))
47
 
48
+ # Highlight unavailable cells where capacity was seen within this window.
49
+ RECENT_AVAILABILITY_DAYS = 10
50
+
51
  # Optional background refresh of all types/regions. 0 = disabled (probe on
52
  # demand only). Each full sweep creates+cancels one reservation per offered AZ.
53
  AUTO_REFRESH_MINUTES = int(os.environ.get("AUTO_REFRESH_MINUTES", "0"))
gpu_capacity/history.py CHANGED
@@ -14,6 +14,8 @@ RECORDED = {"available", "unavailable", "quota", "forbidden"}
14
 
15
  # (instance_type, region) -> [[ts, available_azs, total_azs, status], ...]
16
  _hist: dict[tuple[str, str], list] = {}
 
 
17
  # today's raw jsonl lines, kept whole: the day file is always rewritten in
18
  # full by a dedicated writer thread. Appending from the sweep thread is what
19
  # froze the sweeps — O_APPEND is unreliable on the FUSE bucket mount, and a
@@ -43,6 +45,9 @@ def load():
43
  _hist.setdefault((r["type"], r["region"]), []).append(
44
  [r["t"], r["a"], r["n"], r["s"]]
45
  )
 
 
 
46
  rows += 1
47
  if fn[:-6] == _today_day[0]:
48
  _today.append(line.rstrip("\n"))
@@ -68,6 +73,9 @@ def record(instance_type: str, results: dict[str, dict]):
68
  _hist.setdefault((instance_type, region), []).append(
69
  [ts, avail, len(azs), result["status"]]
70
  )
 
 
 
71
  _queue.put(json.dumps({
72
  "t": ts, "type": instance_type, "region": region,
73
  "a": avail, "n": len(azs), "s": result["status"],
@@ -99,6 +107,17 @@ def _writer_loop():
99
  log.warning("history write failed: %s", e)
100
 
101
 
 
 
 
 
 
 
 
 
 
 
 
102
  def series(instance_type: str, region: str, hours: float) -> list[dict]:
103
  cutoff = time.time() - hours * 3600
104
  with _lock:
 
14
 
15
  # (instance_type, region) -> [[ts, available_azs, total_azs, status], ...]
16
  _hist: dict[tuple[str, str], list] = {}
17
+ # Latest positive observation, indexed for the frequently polled state API.
18
+ _last_available: dict[tuple[str, str], int] = {}
19
  # today's raw jsonl lines, kept whole: the day file is always rewritten in
20
  # full by a dedicated writer thread. Appending from the sweep thread is what
21
  # froze the sweeps — O_APPEND is unreliable on the FUSE bucket mount, and a
 
45
  _hist.setdefault((r["type"], r["region"]), []).append(
46
  [r["t"], r["a"], r["n"], r["s"]]
47
  )
48
+ if r["s"] == "available" and r["a"] > 0:
49
+ key = (r["type"], r["region"])
50
+ _last_available[key] = max(_last_available.get(key, 0), r["t"])
51
  rows += 1
52
  if fn[:-6] == _today_day[0]:
53
  _today.append(line.rstrip("\n"))
 
73
  _hist.setdefault((instance_type, region), []).append(
74
  [ts, avail, len(azs), result["status"]]
75
  )
76
+ if result["status"] == "available" and avail > 0:
77
+ key = (instance_type, region)
78
+ _last_available[key] = max(_last_available.get(key, 0), ts)
79
  _queue.put(json.dumps({
80
  "t": ts, "type": instance_type, "region": region,
81
  "a": avail, "n": len(azs), "s": result["status"],
 
107
  log.warning("history write failed: %s", e)
108
 
109
 
110
+ def recent_availability(days: int) -> dict[str, dict[str, int]]:
111
+ now = time.time()
112
+ cutoff = now - days * 86400
113
+ recent: dict[str, dict[str, int]] = {}
114
+ with _lock:
115
+ for (instance_type, region), ts in _last_available.items():
116
+ if cutoff <= ts <= now:
117
+ recent.setdefault(instance_type, {})[region] = ts
118
+ return recent
119
+
120
+
121
  def series(instance_type: str, region: str, hours: float) -> list[dict]:
122
  cutoff = time.time() - hours * 3600
123
  with _lock:
gpu_capacity/main.py CHANGED
@@ -100,6 +100,8 @@ def get_state():
100
  "instance_types": types,
101
  "regions": config.REGIONS,
102
  "stale_after_seconds": config.STALE_AFTER_SECONDS,
 
 
103
  "gpu_info": prober.gpu_info(types),
104
  "state": current,
105
  }
 
100
  "instance_types": types,
101
  "regions": config.REGIONS,
102
  "stale_after_seconds": config.STALE_AFTER_SECONDS,
103
+ "recent_availability_days": config.RECENT_AVAILABILITY_DAYS,
104
+ "recent_availability": history.recent_availability(config.RECENT_AVAILABILITY_DAYS),
105
  "gpu_info": prober.gpu_info(types),
106
  "state": current,
107
  }
gpu_capacity/static/index.html CHANGED
@@ -24,6 +24,8 @@
24
  --ok-soft: #e9f4ed;
25
  --ko: #b3453c;
26
  --ko-soft: #f7ecea;
 
 
27
  --warn: #966916;
28
  --warn-soft: #f7f1e5;
29
  --na: #7d8782;
@@ -50,6 +52,8 @@
50
  --ok-soft: #18291f;
51
  --ko: #e18b83;
52
  --ko-soft: #2c201f;
 
 
53
  --warn: #d9b468;
54
  --warn-soft: #2d281d;
55
  --na: #9ba59f;
@@ -412,6 +416,10 @@
412
  background: var(--ko);
413
  }
414
 
 
 
 
 
415
  .signal.quota,
416
  .signal.forbidden {
417
  background: var(--warn);
@@ -621,6 +629,10 @@
621
  background: var(--ko-soft);
622
  }
623
 
 
 
 
 
624
  .matrix-cell.quota .cell-button,
625
  .matrix-cell.forbidden .cell-button {
626
  background: var(--warn-soft);
@@ -654,6 +666,10 @@
654
  color: var(--ko);
655
  }
656
 
 
 
 
 
657
  .matrix-cell.quota .cell-value,
658
  .matrix-cell.forbidden .cell-value {
659
  color: var(--warn);
@@ -858,6 +874,12 @@
858
  font-size: 12px;
859
  }
860
 
 
 
 
 
 
 
861
  #p-summary {
862
  display: flex;
863
  align-items: center;
@@ -1146,6 +1168,7 @@
1146
  <span class="legend-item"><span class="signal available"></span>Available</span>
1147
  <span class="legend-item"><span class="signal partial"></span>Partial AZ coverage</span>
1148
  <span class="legend-item"><span class="signal unavailable"></span>No capacity</span>
 
1149
  <span class="legend-item"><span class="signal quota"></span>Quota or denied</span>
1150
  <span class="legend-item"><span class="signal unsupported"></span>N.O. / n/s / stale</span>
1151
  </div>
@@ -1173,6 +1196,7 @@
1173
  <div id="p-age"></div>
1174
  </div>
1175
  <div id="p-summary"></div>
 
1176
  <div id="p-azs"></div>
1177
  <button class="button primary" id="p-probe" type="button">Probe region now</button>
1178
  </section>
@@ -1184,6 +1208,7 @@
1184
  <button type="button" data-hours="24">24h</button>
1185
  <button type="button" data-hours="48" class="active">48h</button>
1186
  <button type="button" data-hours="168">7d</button>
 
1187
  </div>
1188
  </div>
1189
  <div id="chartwrap"><div id="tooltip"></div></div>
@@ -1250,7 +1275,14 @@ function humanStatus(status) {
1250
  }[status] || status;
1251
  }
1252
 
1253
- function cellModel(result) {
 
 
 
 
 
 
 
1254
  if (!result) {
1255
  return {
1256
  cls: "never",
@@ -1264,11 +1296,14 @@ function cellModel(result) {
1264
  }
1265
 
1266
  const counts = azCounts(result);
1267
- const status = result.status === "available" && counts.available < counts.total ? "partial" : result.status;
 
 
1268
  const label = {
1269
  available: `${counts.available}/${counts.total}`,
1270
  partial: `${counts.available}/${counts.total}`,
1271
  unavailable: counts.total ? `0/${counts.total}` : "none",
 
1272
  quota: "quota",
1273
  forbidden: "denied",
1274
  unsupported: "n/s",
@@ -1284,8 +1319,8 @@ function cellModel(result) {
1284
  status,
1285
  label,
1286
  age: fmtAge(result.checked_at),
1287
- title,
1288
- detail: humanStatus(status),
1289
  count,
1290
  };
1291
  }
@@ -1407,6 +1442,7 @@ function renderMatrix(data) {
1407
  const empty = document.getElementById("empty-filter");
1408
  const copy = document.getElementById("matrix-copy");
1409
  copy.textContent = `${visibleTypes.length} of ${data.instance_types.length} instance types across ${data.regions.length} regions`;
 
1410
 
1411
  const colgroup = `
1412
  <colgroup>
@@ -1453,7 +1489,7 @@ function renderMatrix(data) {
1453
  </th>
1454
  ${displayRegions.map(region => {
1455
  const result = (data.state[type] || {})[region];
1456
- const model = cellModel(result);
1457
  const probing = isProbing(type, region);
1458
  const aria = `${type} in ${region}: ${model.detail}${model.count ? `, ${model.count}` : ""}, ${model.age}`;
1459
  return `<td class="matrix-cell ${esc(model.cls)}${probing ? " probing" : ""}" data-type="${esc(type)}" data-region="${esc(region)}">
@@ -1531,6 +1567,12 @@ function renderPanelStatus() {
1531
  const age = document.getElementById("p-age");
1532
  const summary = document.getElementById("p-summary");
1533
  const box = document.getElementById("p-azs");
 
 
 
 
 
 
1534
  if (!result) {
1535
  age.textContent = "";
1536
  summary.innerHTML = `<span class="panel-status"><span class="signal never"></span>Never probed</span>`;
 
24
  --ok-soft: #e9f4ed;
25
  --ko: #b3453c;
26
  --ko-soft: #f7ecea;
27
+ --recent: #245fb5;
28
+ --recent-soft: #eaf1fc;
29
  --warn: #966916;
30
  --warn-soft: #f7f1e5;
31
  --na: #7d8782;
 
52
  --ok-soft: #18291f;
53
  --ko: #e18b83;
54
  --ko-soft: #2c201f;
55
+ --recent: #8ab7fb;
56
+ --recent-soft: #1b293d;
57
  --warn: #d9b468;
58
  --warn-soft: #2d281d;
59
  --na: #9ba59f;
 
416
  background: var(--ko);
417
  }
418
 
419
+ .signal.recent {
420
+ background: var(--recent);
421
+ }
422
+
423
  .signal.quota,
424
  .signal.forbidden {
425
  background: var(--warn);
 
629
  background: var(--ko-soft);
630
  }
631
 
632
+ .matrix-cell.recent .cell-button {
633
+ background: var(--recent-soft);
634
+ }
635
+
636
  .matrix-cell.quota .cell-button,
637
  .matrix-cell.forbidden .cell-button {
638
  background: var(--warn-soft);
 
666
  color: var(--ko);
667
  }
668
 
669
+ .matrix-cell.recent .cell-value {
670
+ color: var(--recent);
671
+ }
672
+
673
  .matrix-cell.quota .cell-value,
674
  .matrix-cell.forbidden .cell-value {
675
  color: var(--warn);
 
874
  font-size: 12px;
875
  }
876
 
877
+ #p-last-available {
878
+ color: var(--recent);
879
+ font-size: 12px;
880
+ line-height: 1.5;
881
+ }
882
+
883
  #p-summary {
884
  display: flex;
885
  align-items: center;
 
1168
  <span class="legend-item"><span class="signal available"></span>Available</span>
1169
  <span class="legend-item"><span class="signal partial"></span>Partial AZ coverage</span>
1170
  <span class="legend-item"><span class="signal unavailable"></span>No capacity</span>
1171
+ <span class="legend-item"><span class="signal recent"></span><span id="recent-legend">Seen in past 10d; unavailable now</span></span>
1172
  <span class="legend-item"><span class="signal quota"></span>Quota or denied</span>
1173
  <span class="legend-item"><span class="signal unsupported"></span>N.O. / n/s / stale</span>
1174
  </div>
 
1196
  <div id="p-age"></div>
1197
  </div>
1198
  <div id="p-summary"></div>
1199
+ <p id="p-last-available" hidden></p>
1200
  <div id="p-azs"></div>
1201
  <button class="button primary" id="p-probe" type="button">Probe region now</button>
1202
  </section>
 
1208
  <button type="button" data-hours="24">24h</button>
1209
  <button type="button" data-hours="48" class="active">48h</button>
1210
  <button type="button" data-hours="168">7d</button>
1211
+ <button type="button" data-hours="240">10d</button>
1212
  </div>
1213
  </div>
1214
  <div id="chartwrap"><div id="tooltip"></div></div>
 
1275
  }[status] || status;
1276
  }
1277
 
1278
+ function recentAvailabilityAt(type, region) {
1279
+ const ts = ((META.recent_availability || {})[type] || {})[region];
1280
+ const now = Date.now() / 1000;
1281
+ const cutoff = now - META.recent_availability_days * 86400;
1282
+ return Number.isFinite(ts) && cutoff <= ts && ts <= now ? ts : null;
1283
+ }
1284
+
1285
+ function cellModel(result, lastAvailableAt = null) {
1286
  if (!result) {
1287
  return {
1288
  cls: "never",
 
1296
  }
1297
 
1298
  const counts = azCounts(result);
1299
+ const status = result.status === "unavailable" && lastAvailableAt ? "recent"
1300
+ : result.status === "available" && counts.available < counts.total ? "partial" : result.status;
1301
+ const lastSeen = lastAvailableAt ? new Date(lastAvailableAt * 1000) : null;
1302
  const label = {
1303
  available: `${counts.available}/${counts.total}`,
1304
  partial: `${counts.available}/${counts.total}`,
1305
  unavailable: counts.total ? `0/${counts.total}` : "none",
1306
+ recent: `Seen ${lastSeen?.toLocaleDateString([], { day: "numeric", month: "short" })}`,
1307
  quota: "quota",
1308
  forbidden: "denied",
1309
  unsupported: "n/s",
 
1319
  status,
1320
  label,
1321
  age: fmtAge(result.checked_at),
1322
+ title: lastSeen ? `${humanStatus(result.status)}. Last available: ${lastSeen.toLocaleString()}\n${title}` : title,
1323
+ detail: status === "recent" ? `No capacity now; last available ${lastSeen.toLocaleString()}` : humanStatus(status),
1324
  count,
1325
  };
1326
  }
 
1442
  const empty = document.getElementById("empty-filter");
1443
  const copy = document.getElementById("matrix-copy");
1444
  copy.textContent = `${visibleTypes.length} of ${data.instance_types.length} instance types across ${data.regions.length} regions`;
1445
+ document.getElementById("recent-legend").textContent = `Seen in past ${data.recent_availability_days}d; unavailable now`;
1446
 
1447
  const colgroup = `
1448
  <colgroup>
 
1489
  </th>
1490
  ${displayRegions.map(region => {
1491
  const result = (data.state[type] || {})[region];
1492
+ const model = cellModel(result, recentAvailabilityAt(type, region));
1493
  const probing = isProbing(type, region);
1494
  const aria = `${type} in ${region}: ${model.detail}${model.count ? `, ${model.count}` : ""}, ${model.age}`;
1495
  return `<td class="matrix-cell ${esc(model.cls)}${probing ? " probing" : ""}" data-type="${esc(type)}" data-region="${esc(region)}">
 
1567
  const age = document.getElementById("p-age");
1568
  const summary = document.getElementById("p-summary");
1569
  const box = document.getElementById("p-azs");
1570
+ const lastAvailableAt = recentAvailabilityAt(PANEL.type, PANEL.region);
1571
+ const historyNote = document.getElementById("p-last-available");
1572
+ historyNote.hidden = !lastAvailableAt;
1573
+ historyNote.textContent = lastAvailableAt
1574
+ ? `Capacity last observed ${new Date(lastAvailableAt * 1000).toLocaleString()} (within the past ${META.recent_availability_days} days).`
1575
+ : "";
1576
  if (!result) {
1577
  age.textContent = "";
1578
  summary.innerHTML = `<span class="panel-status"><span class="signal never"></span>Never probed</span>`;
tests/test_recent_availability.py ADDED
@@ -0,0 +1,93 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import queue
3
+ import tempfile
4
+ import unittest
5
+ from pathlib import Path
6
+ from unittest.mock import patch
7
+
8
+ from gpu_capacity import history, main
9
+
10
+
11
+ class RecentAvailabilityTests(unittest.TestCase):
12
+ def setUp(self):
13
+ self.now = 1791417600 # 2026-10-08 00:00 UTC
14
+ self.cutoff = self.now - 10 * 86400
15
+ for name in ("_hist", "_last_available"):
16
+ mock = patch.dict(getattr(history, name), {}, clear=True)
17
+ mock.start()
18
+ self.addCleanup(mock.stop)
19
+ for name, value in (("_queue", queue.Queue()), ("_today", []), ("_today_day", [""])):
20
+ mock = patch.object(history, name, value)
21
+ mock.start()
22
+ self.addCleanup(mock.stop)
23
+ clock = patch.object(history.time, "time", return_value=self.now)
24
+ clock.start()
25
+ self.addCleanup(clock.stop)
26
+
27
+ def result(self, ts, status="available", available=True):
28
+ return {
29
+ "checked_at": ts,
30
+ "status": status,
31
+ "azs": {"us-east-1a": {"status": "available" if available else "unavailable"}},
32
+ }
33
+
34
+ def test_positive_observation_survives_failures_and_out_of_order_results(self):
35
+ for result in (
36
+ self.result(self.now - 100),
37
+ self.result(self.now - 50, "unavailable", False),
38
+ self.result(self.now - 200),
39
+ self.result(self.now - 25, "quota", False),
40
+ self.result(self.now, "available", False),
41
+ ):
42
+ history.record("g7e.2xlarge", {"us-east-1": result})
43
+ self.assertEqual(
44
+ history.recent_availability(10),
45
+ {"g7e.2xlarge": {"us-east-1": self.now - 100}},
46
+ )
47
+
48
+ def test_window_boundary_and_expiration_without_new_probes(self):
49
+ history._last_available.update({
50
+ ("g7e.2xlarge", "us-east-1"): self.cutoff,
51
+ ("g7e.2xlarge", "us-east-2"): self.cutoff - 1,
52
+ ("g7e.48xlarge", "us-east-1"): self.now + 1,
53
+ })
54
+ self.assertEqual(
55
+ history.recent_availability(10),
56
+ {"g7e.2xlarge": {"us-east-1": self.cutoff}},
57
+ )
58
+ with patch.object(history.time, "time", return_value=self.now + 0.5):
59
+ self.assertEqual(history.recent_availability(10), {})
60
+
61
+ def test_restore_history_finds_latest_success_not_latest_probe(self):
62
+ observations = [
63
+ (self.now - 86400, 1, "available"),
64
+ (self.now - 2 * 86400, 2, "available"),
65
+ (self.now - 100, 0, "unavailable"),
66
+ (self.now - 50, 0, "available"),
67
+ (self.now, 1, "quota"),
68
+ ]
69
+ with tempfile.TemporaryDirectory() as directory:
70
+ path = Path(directory) / "2026-10-07.jsonl"
71
+ path.write_text("\n".join(json.dumps({
72
+ "t": ts, "a": available, "n": 2, "s": status,
73
+ "type": "g7e.2xlarge", "region": "us-east-1",
74
+ }) for ts, available, status in observations))
75
+ with patch.object(history.config, "HISTORY_DIR", directory):
76
+ history.load()
77
+ self.assertEqual(
78
+ history.recent_availability(10),
79
+ {"g7e.2xlarge": {"us-east-1": self.now - 86400}},
80
+ )
81
+
82
+ def test_state_api_exposes_history_without_changing_current_status(self):
83
+ history.record("g7e.2xlarge", {"us-east-1": self.result(self.now - 86400)})
84
+ current = {"g7e.2xlarge": {"us-east-1": self.result(self.now, "unavailable", False)}}
85
+ with patch.object(main.state, "get", return_value=current), patch.object(main.prober, "gpu_info", return_value={}):
86
+ response = main.get_state()
87
+ self.assertEqual(response["recent_availability_days"], 10)
88
+ self.assertEqual(response["recent_availability"]["g7e.2xlarge"]["us-east-1"], self.now - 86400)
89
+ self.assertEqual(response["state"]["g7e.2xlarge"]["us-east-1"]["status"], "unavailable")
90
+
91
+
92
+ if __name__ == "__main__":
93
+ unittest.main()
tests/test_ui.cjs ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const assert = require("node:assert/strict");
2
+ const fs = require("node:fs");
3
+ const path = require("node:path");
4
+ const test = require("node:test");
5
+ const vm = require("node:vm");
6
+
7
+ const html = fs.readFileSync(path.join(__dirname, "../gpu_capacity/static/index.html"), "utf8");
8
+ const script = html.match(/<script>([\s\S]*?)<\/script>/)[1]
9
+ .split('document.getElementById("grid").addEventListener')[0];
10
+ const now = Date.UTC(2026, 9, 8) / 1000;
11
+
12
+ function setup(status = "unavailable", lastSeen = now - 86400) {
13
+ const elements = {};
14
+ const context = vm.createContext({
15
+ Date: class extends Date { static now() { return now * 1000; } },
16
+ document: { getElementById(id) {
17
+ return elements[id] ||= { classList: { toggle() {} }, style: {}, textContent: "", innerHTML: "" };
18
+ } },
19
+ });
20
+ vm.runInContext(script, context);
21
+ const data = {
22
+ instance_types: ["g7e.2xlarge"], regions: ["us-east-1"], gpu_info: {},
23
+ stale_after_seconds: 900, recent_availability_days: 10,
24
+ recent_availability: { "g7e.2xlarge": { "us-east-1": lastSeen } },
25
+ state: { "g7e.2xlarge": { "us-east-1": {
26
+ status, checked_at: now - 30,
27
+ azs: { "us-east-1a": { status }, "us-east-1b": { status } },
28
+ } } },
29
+ };
30
+ vm.runInContext(`META = ${JSON.stringify(data)}; PANEL.type = "g7e.2xlarge"; PANEL.region = "us-east-1";`, context);
31
+ return { context, elements, run: source => vm.runInContext(source, context) };
32
+ }
33
+
34
+ function model(ui) {
35
+ return ui.run('cellModel(META.state["g7e.2xlarge"]["us-east-1"], recentAvailabilityAt("g7e.2xlarge", "us-east-1"))');
36
+ }
37
+
38
+ test("unavailable cells with recent capacity turn blue and disclose the date", () => {
39
+ const ui = setup();
40
+ const cell = model(ui);
41
+ assert.equal(cell.status, "recent");
42
+ assert.match(cell.label, /^Seen /);
43
+ assert.match(cell.detail, /No capacity now; last available/);
44
+ ui.run("renderMatrix(META); renderPanelStatus();");
45
+ assert.match(ui.elements.grid.innerHTML, /matrix-cell recent/);
46
+ assert.match(ui.elements["p-summary"].innerHTML, /No capacity/);
47
+ assert.match(ui.elements["p-last-available"].textContent, /Capacity last observed/);
48
+ assert.equal(ui.elements["p-last-available"].hidden, false);
49
+ assert.match(ui.elements["recent-legend"].textContent, /10d/);
50
+ });
51
+
52
+ test("fresh capacity and quota failures retain their current colors", () => {
53
+ for (const status of ["available", "quota", "forbidden", "error", "unsupported", "not_offered"]) {
54
+ assert.equal(model(setup(status)).status, status);
55
+ }
56
+ const ui = setup("available");
57
+ ui.run('META.state["g7e.2xlarge"]["us-east-1"].azs["us-east-1b"].status = "unavailable"');
58
+ assert.equal(model(ui).status, "partial");
59
+ });
60
+
61
+ test("missing, future, or expired observations stay red, including after the time window expires", () => {
62
+ for (const timestamp of [null, now + 1, now - 10 * 86400 - 1]) {
63
+ assert.equal(model(setup("unavailable", timestamp)).status, "unavailable");
64
+ }
65
+ const ui = setup("unavailable", now - 10 * 86400);
66
+ assert.equal(model(ui).status, "recent");
67
+ ui.run(`Date.now = () => ${(now + 1) * 1000}`);
68
+ assert.equal(model(ui).status, "unavailable");
69
+ });
70
+
71
+ test("historical capacity is not counted or filtered as current availability", () => {
72
+ const ui = setup();
73
+ ui.run("VIEW.availableOnly = true;");
74
+ assert.equal(ui.run("filteredTypes(META).length"), 0);
75
+ assert.equal(ui.run('coverageSummary(META, "g7e.2xlarge").available'), 0);
76
+ assert.match(html, /data-hours="240">10d/);
77
+ });