1
0
Fork 0
mirror of https://github.com/imjasonh/terraform-playground synced 2026-07-08 07:44:57 +00:00

UI: spot savings multiplier, AWS pool prices, scrollable node grid

- Show the spot price as a <1× multiplier of on-demand (e.g. 0.42×) with a
  'spot vs on-dmd' label, colored by how much you're saving; node spot tags
  show the effective $/hr and savings %.
- Node-pool dropdown shows realistic specs and prices (spot priced ~0.45×).
- Make the node and queue panels flex columns whose lists scroll
  internally, so a large cluster no longer overflows its row and covers the
  event log.

Co-authored-by: Jason Hall <imjasonh@users.noreply.github.com>
This commit is contained in:
Cursor Agent 2026-06-26 14:04:16 +00:00
parent 3338b31e1a
commit 80538d1509
No known key found for this signature in database
2 changed files with 22 additions and 10 deletions

View file

@ -46,12 +46,14 @@ export class UI {
nodeSel.innerHTML = Object.values(INSTANCE_TYPES) nodeSel.innerHTML = Object.values(INSTANCE_TYPES)
.map((t) => { .map((t) => {
const gpu = t.gpu ? `, ${t.gpu} GPU` : ""; const gpu = t.gpu ? `, ${t.gpu} GPU` : "";
const price =
t.family === "spot" ? `~$${(t.cost * 0.45).toFixed(2)}/hr spot` : `$${t.cost.toFixed(2)}/hr`;
return `<option value="${t.key}">${t.key}${fmtCpu(t.cpu)} vCPU / ${fmtMem( return `<option value="${t.key}">${t.key}${fmtCpu(t.cpu)} vCPU / ${fmtMem(
t.mem t.mem
)}${gpu} · $${t.cost.toFixed(2)}/hr</option>`; )}${gpu} · ${price}</option>`;
}) })
.join(""); .join("");
nodeSel.value = "general-large"; nodeSel.value = "c5.2xlarge";
} }
wire() { wire() {
@ -285,7 +287,7 @@ export class UI {
{ l: "Running", v: g.runningCount(), cls: "" }, { l: "Running", v: g.runningCount(), cls: "" },
{ l: "Nodes", v: `${ready}/${s.nodes.length}`, cls: "" }, { l: "Nodes", v: `${ready}/${s.nodes.length}`, cls: "" },
{ l: "Cost", v: `$${g.hourlyCost().toFixed(2)}/hr`, cls: "" }, { l: "Cost", v: `$${g.hourlyCost().toFixed(2)}/hr`, cls: "" },
{ l: "Spot price", v: `${spot.toFixed(2)}×`, cls: spot >= 1.5 ? "bad" : spot <= 0.8 ? "good" : "" }, { l: "spot vs on-dmd", v: `${spot.toFixed(2)}×`, cls: spot <= 0.5 ? "good" : spot >= 0.75 ? "bad" : "" },
{ {
l: s.upgradePending ? `upgrade: ${outdated} left` : "k8s version", l: s.upgradePending ? `upgrade: ${outdated} left` : "k8s version",
v: `v1.${s.clusterMinor}`, v: `v1.${s.clusterMinor}`,
@ -346,7 +348,9 @@ export class UI {
.map((t) => `<span class="tag taint">⛔ ${t.key}=${t.value}:${t.effect}</span>`) .map((t) => `<span class="tag taint">⛔ ${t.key}=${t.value}:${t.effect}</span>`)
.join(""); .join("");
const spotTag = node.spot const spotTag = node.spot
? `<span class="tag spot">⚡ spot $${(node.cost * s.spotPrice).toFixed(2)}/hr</span>` ? `<span class="tag spot">⚡ spot $${(node.cost * s.spotPrice).toFixed(2)}/hr · ${Math.round(
(1 - s.spotPrice) * 100
)}%</span>`
: ""; : "";
const cpuBar = this.meter("CPU", used.cpu, node.cpu, meterPods, "cpu"); const cpuBar = this.meter("CPU", used.cpu, node.cpu, meterPods, "cpu");

View file

@ -271,7 +271,7 @@ button:focus-visible {
grid-template-columns: 1fr 340px; grid-template-columns: 1fr 340px;
gap: 16px; gap: 16px;
padding: 16px 22px; padding: 16px 22px;
align-items: start; align-items: stretch;
min-height: 0; min-height: 0;
} }
.panel { .panel {
@ -292,11 +292,22 @@ button:focus-visible {
letter-spacing: 0.3px; letter-spacing: 0.3px;
} }
/* The node and queue panels are flex columns whose lists scroll internally,
so a long cluster never overflows its row and covers the event log. */
.nodes,
.queue {
display: flex;
flex-direction: column;
min-height: 0;
}
.nodegrid { .nodegrid {
display: grid; display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 14px; gap: 14px;
padding: 16px; padding: 16px;
flex: 1 1 auto;
min-height: 0;
overflow-y: auto;
} }
/* --------------------------------------------------------------- node card */ /* --------------------------------------------------------------- node card */
@ -543,10 +554,6 @@ button:focus-visible {
} }
/* --------------------------------------------------------------- queue */ /* --------------------------------------------------------------- queue */
.queue {
position: sticky;
top: 16px;
}
.hint { .hint {
margin: 0; margin: 0;
padding: 8px 16px; padding: 8px 16px;
@ -559,7 +566,8 @@ button:focus-visible {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 8px; gap: 8px;
max-height: calc(100vh - 360px); flex: 1 1 auto;
min-height: 0;
overflow-y: auto; overflow-y: auto;
} }
.empty-state { .empty-state {