@hotcell/daemon 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. package/dist/agent-check.d.ts +1 -0
  2. package/dist/agent-check.js +105 -0
  3. package/dist/agent-check.js.map +1 -0
  4. package/dist/api/server.d.ts +64 -0
  5. package/dist/api/server.js +1368 -0
  6. package/dist/api/server.js.map +1 -0
  7. package/dist/api/ws.d.ts +14 -0
  8. package/dist/api/ws.js +214 -0
  9. package/dist/api/ws.js.map +1 -0
  10. package/dist/applevz-check.d.ts +1 -0
  11. package/dist/applevz-check.js +280 -0
  12. package/dist/applevz-check.js.map +1 -0
  13. package/dist/backups.d.ts +29 -0
  14. package/dist/backups.js +76 -0
  15. package/dist/backups.js.map +1 -0
  16. package/dist/capacity.d.ts +63 -0
  17. package/dist/capacity.js +95 -0
  18. package/dist/capacity.js.map +1 -0
  19. package/dist/config.d.ts +241 -0
  20. package/dist/config.js +152 -0
  21. package/dist/config.js.map +1 -0
  22. package/dist/cost.d.ts +9 -0
  23. package/dist/cost.js +19 -0
  24. package/dist/cost.js.map +1 -0
  25. package/dist/density-bench.d.ts +1 -0
  26. package/dist/density-bench.js +97 -0
  27. package/dist/density-bench.js.map +1 -0
  28. package/dist/driver/agent-driver.d.ts +67 -0
  29. package/dist/driver/agent-driver.js +197 -0
  30. package/dist/driver/agent-driver.js.map +1 -0
  31. package/dist/driver/agent.d.ts +94 -0
  32. package/dist/driver/agent.js +319 -0
  33. package/dist/driver/agent.js.map +1 -0
  34. package/dist/driver/applevz.d.ts +95 -0
  35. package/dist/driver/applevz.js +527 -0
  36. package/dist/driver/applevz.js.map +1 -0
  37. package/dist/driver/container.d.ts +99 -0
  38. package/dist/driver/container.js +733 -0
  39. package/dist/driver/container.js.map +1 -0
  40. package/dist/driver/fc-api.d.ts +85 -0
  41. package/dist/driver/fc-api.js +173 -0
  42. package/dist/driver/fc-api.js.map +1 -0
  43. package/dist/driver/firecracker.d.ts +126 -0
  44. package/dist/driver/firecracker.js +560 -0
  45. package/dist/driver/firecracker.js.map +1 -0
  46. package/dist/driver/index.d.ts +17 -0
  47. package/dist/driver/index.js +61 -0
  48. package/dist/driver/index.js.map +1 -0
  49. package/dist/driver/router.d.ts +82 -0
  50. package/dist/driver/router.js +159 -0
  51. package/dist/driver/router.js.map +1 -0
  52. package/dist/driver/types.d.ts +234 -0
  53. package/dist/driver/types.js +2 -0
  54. package/dist/driver/types.js.map +1 -0
  55. package/dist/driver/unsupported.d.ts +50 -0
  56. package/dist/driver/unsupported.js +81 -0
  57. package/dist/driver/unsupported.js.map +1 -0
  58. package/dist/driver/vz-image.d.ts +35 -0
  59. package/dist/driver/vz-image.js +75 -0
  60. package/dist/driver/vz-image.js.map +1 -0
  61. package/dist/e2e-harness.d.ts +1 -0
  62. package/dist/e2e-harness.js +99 -0
  63. package/dist/e2e-harness.js.map +1 -0
  64. package/dist/egress-check.d.ts +8 -0
  65. package/dist/egress-check.js +317 -0
  66. package/dist/egress-check.js.map +1 -0
  67. package/dist/fc-check.d.ts +1 -0
  68. package/dist/fc-check.js +113 -0
  69. package/dist/fc-check.js.map +1 -0
  70. package/dist/index.d.ts +2 -0
  71. package/dist/index.js +114 -0
  72. package/dist/index.js.map +1 -0
  73. package/dist/kernels.d.ts +22 -0
  74. package/dist/kernels.js +134 -0
  75. package/dist/kernels.js.map +1 -0
  76. package/dist/lifecycle.d.ts +49 -0
  77. package/dist/lifecycle.js +111 -0
  78. package/dist/lifecycle.js.map +1 -0
  79. package/dist/logger.d.ts +16 -0
  80. package/dist/logger.js +67 -0
  81. package/dist/logger.js.map +1 -0
  82. package/dist/metrics.d.ts +46 -0
  83. package/dist/metrics.js +92 -0
  84. package/dist/metrics.js.map +1 -0
  85. package/dist/net/firewall.d.ts +4 -0
  86. package/dist/net/firewall.js +80 -0
  87. package/dist/net/firewall.js.map +1 -0
  88. package/dist/pricing.d.ts +36 -0
  89. package/dist/pricing.js +81 -0
  90. package/dist/pricing.js.map +1 -0
  91. package/dist/proxy/allowlist-default.d.ts +23 -0
  92. package/dist/proxy/allowlist-default.js +53 -0
  93. package/dist/proxy/allowlist-default.js.map +1 -0
  94. package/dist/proxy/allowlist.d.ts +43 -0
  95. package/dist/proxy/allowlist.js +87 -0
  96. package/dist/proxy/allowlist.js.map +1 -0
  97. package/dist/proxy/egress.d.ts +49 -0
  98. package/dist/proxy/egress.js +599 -0
  99. package/dist/proxy/egress.js.map +1 -0
  100. package/dist/proxy/ratelimit.d.ts +49 -0
  101. package/dist/proxy/ratelimit.js +74 -0
  102. package/dist/proxy/ratelimit.js.map +1 -0
  103. package/dist/proxy/server.d.ts +26 -0
  104. package/dist/proxy/server.js +202 -0
  105. package/dist/proxy/server.js.map +1 -0
  106. package/dist/router-check.d.ts +1 -0
  107. package/dist/router-check.js +97 -0
  108. package/dist/router-check.js.map +1 -0
  109. package/dist/smoke-fc.d.ts +1 -0
  110. package/dist/smoke-fc.js +183 -0
  111. package/dist/smoke-fc.js.map +1 -0
  112. package/dist/smoke-microvm.d.ts +1 -0
  113. package/dist/smoke-microvm.js +187 -0
  114. package/dist/smoke-microvm.js.map +1 -0
  115. package/dist/smoke-remote.d.ts +20 -0
  116. package/dist/smoke-remote.js +155 -0
  117. package/dist/smoke-remote.js.map +1 -0
  118. package/dist/smoke-vz.d.ts +1 -0
  119. package/dist/smoke-vz.js +161 -0
  120. package/dist/smoke-vz.js.map +1 -0
  121. package/dist/smoke.d.ts +7 -0
  122. package/dist/smoke.js +962 -0
  123. package/dist/smoke.js.map +1 -0
  124. package/dist/store.d.ts +124 -0
  125. package/dist/store.js +578 -0
  126. package/dist/store.js.map +1 -0
  127. package/dist/tracing.d.ts +45 -0
  128. package/dist/tracing.js +144 -0
  129. package/dist/tracing.js.map +1 -0
  130. package/dist/types.d.ts +274 -0
  131. package/dist/types.js +3 -0
  132. package/dist/types.js.map +1 -0
  133. package/dist/util.d.ts +27 -0
  134. package/dist/util.js +63 -0
  135. package/dist/util.js.map +1 -0
  136. package/dist/warmpool-check.d.ts +1 -0
  137. package/dist/warmpool-check.js +83 -0
  138. package/dist/warmpool-check.js.map +1 -0
  139. package/dist/web/dashboard.d.ts +1 -0
  140. package/dist/web/dashboard.js +456 -0
  141. package/dist/web/dashboard.js.map +1 -0
  142. package/package.json +53 -0
@@ -0,0 +1,456 @@
1
+ // Embedded single-page dashboard for hotcell — dependency-free vanilla JS + CSS,
2
+ // served by the daemon at GET /. No build step: the markup is a plain string so
3
+ // it compiles straight to dist with the rest of the daemon. It talks to the same
4
+ // REST API the SDK/CLI use (same origin, so no CORS), polling the sandbox list +
5
+ // cost and showing live metrics for the selected sandbox.
6
+ //
7
+ // Implementation note: this is authored without JS template literals/backticks
8
+ // on purpose, so it can live inside this TS template-literal export without an
9
+ // escaping minefield. String concatenation it is.
10
+ export const DASHBOARD_HTML = String.raw `<!doctype html>
11
+ <html lang="en">
12
+ <head>
13
+ <meta charset="utf-8" />
14
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
15
+ <title>hotcell dashboard</title>
16
+ <style>
17
+ :root {
18
+ --bg: #0d1117; --panel: #161b22; --border: #30363d; --fg: #e6edf3;
19
+ --muted: #8b949e; --accent: #2f81f7; --green: #3fb950; --yellow: #d29922;
20
+ --red: #f85149; --mono: ui-monospace, SFMono-Regular, Menlo, monospace;
21
+ }
22
+ * { box-sizing: border-box; }
23
+ body { margin: 0; background: var(--bg); color: var(--fg);
24
+ font: 14px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
25
+ header { display: flex; align-items: center; gap: 16px; padding: 14px 20px;
26
+ border-bottom: 1px solid var(--border); background: var(--panel); }
27
+ header h1 { font-size: 16px; margin: 0; letter-spacing: .5px; }
28
+ header .meta { color: var(--muted); font-size: 12px; }
29
+ header .spacer { flex: 1; }
30
+ header .cost { font-family: var(--mono); font-size: 13px; }
31
+ header .cost b { color: var(--green); }
32
+ main { padding: 20px; max-width: 1200px; margin: 0 auto; }
33
+ .bar { display: flex; gap: 8px; margin-bottom: 14px; align-items: center; }
34
+ input, button, select { font: inherit; }
35
+ input, select { background: var(--bg); color: var(--fg);
36
+ border: 1px solid var(--border); border-radius: 6px; padding: 6px 8px; }
37
+ button { background: var(--accent); color: #fff; border: 0; border-radius: 6px;
38
+ padding: 6px 12px; cursor: pointer; }
39
+ button.secondary { background: #21262d; color: var(--fg); border: 1px solid var(--border); }
40
+ button.danger { background: transparent; color: var(--red); border: 1px solid var(--border); }
41
+ button:hover { filter: brightness(1.1); }
42
+ table { width: 100%; border-collapse: collapse; }
43
+ th, td { text-align: left; padding: 9px 10px; border-bottom: 1px solid var(--border);
44
+ font-size: 13px; }
45
+ th { color: var(--muted); font-weight: 600; font-size: 11px; text-transform: uppercase;
46
+ letter-spacing: .5px; }
47
+ tbody tr { cursor: pointer; }
48
+ tbody tr:hover { background: #1c2230; }
49
+ tbody tr.sel { background: #1f2937; }
50
+ td.id { font-family: var(--mono); }
51
+ .badge { display: inline-block; padding: 1px 8px; border-radius: 999px; font-size: 11px;
52
+ font-weight: 600; }
53
+ .badge.running { background: rgba(63,185,80,.15); color: var(--green); }
54
+ .badge.paused { background: rgba(210,153,34,.15); color: var(--yellow); }
55
+ .badge.stopped { background: rgba(139,148,158,.15); color: var(--muted); }
56
+ .num { font-family: var(--mono); text-align: right; }
57
+ .actions { white-space: nowrap; }
58
+ .actions button { padding: 3px 8px; font-size: 12px; margin-left: 4px; }
59
+ .detail { margin-top: 18px; padding: 16px; background: var(--panel);
60
+ border: 1px solid var(--border); border-radius: 8px; }
61
+ .detail h2 { font-size: 14px; margin: 0 0 10px; font-family: var(--mono); }
62
+ .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
63
+ gap: 12px; }
64
+ .stat { background: var(--bg); border: 1px solid var(--border); border-radius: 6px;
65
+ padding: 10px 12px; }
66
+ .stat .k { color: var(--muted); font-size: 11px; text-transform: uppercase; }
67
+ .stat .v { font-family: var(--mono); font-size: 18px; margin-top: 2px; }
68
+ .ports a { color: var(--accent); font-family: var(--mono); font-size: 12px;
69
+ display: inline-block; margin-right: 12px; }
70
+ .empty { color: var(--muted); padding: 30px; text-align: center; }
71
+ .err { color: var(--red); padding: 8px 0; font-size: 13px; }
72
+ .sparks { display: flex; gap: 16px; margin: 0 0 14px; }
73
+ .sparkbox { background: var(--bg); border: 1px solid var(--border); border-radius: 6px;
74
+ padding: 8px 12px; }
75
+ .sparkbox .k { color: var(--muted); font-size: 11px; text-transform: uppercase;
76
+ margin-bottom: 4px; }
77
+ .spark { display: block; }
78
+ .spark-empty { color: var(--muted); }
79
+ #terminal { margin-top: 18px; }
80
+ #terminal:empty { margin-top: 0; }
81
+ .termbar { display: flex; align-items: center; gap: 10px; padding: 8px 12px;
82
+ background: var(--panel); border: 1px solid var(--border); border-bottom: 0;
83
+ border-radius: 8px 8px 0 0; font-family: var(--mono); font-size: 12px; }
84
+ .termbar .spacer { flex: 1; }
85
+ #termMount { height: 360px; background: #0d1117; border: 1px solid var(--border);
86
+ border-radius: 0 0 8px 8px; padding: 6px; }
87
+ .detail h2 .termbtn { float: right; padding: 2px 10px; font-size: 12px; }
88
+ </style>
89
+ </head>
90
+ <body>
91
+ <header>
92
+ <h1>hotcell</h1>
93
+ <span class="meta" id="info">connecting…</span>
94
+ <span class="spacer"></span>
95
+ <span class="cost" id="cap" title="host memory committed / budget"></span>
96
+ <span class="cost">total cost: <b id="totalCost">$0.000000</b></span>
97
+ </header>
98
+ <main>
99
+ <div class="bar">
100
+ <input id="newImage" placeholder="image (optional)" size="22" />
101
+ <input id="newSleep" placeholder="sleepAfter ms" size="13" />
102
+ <input id="newMem" placeholder="mem MB" size="8" />
103
+ <input id="newCpus" placeholder="cpus" size="6" />
104
+ <input id="newPids" placeholder="pids" size="6" />
105
+ <button id="newBtn">New sandbox</button>
106
+ <span class="spacer" style="flex:1"></span>
107
+ <button class="secondary" id="refreshBtn">Refresh</button>
108
+ </div>
109
+ <div id="err" class="err" style="display:none"></div>
110
+ <table>
111
+ <thead><tr>
112
+ <th>ID</th><th>Status</th><th>Image</th><th>Created</th><th>Last activity</th>
113
+ <th class="num">vCPU-s</th><th class="num">GB-s</th><th class="num">Cost</th><th></th>
114
+ </tr></thead>
115
+ <tbody id="rows"></tbody>
116
+ </table>
117
+ <div id="detail"></div>
118
+ <div id="terminal"></div>
119
+ </main>
120
+ <script>
121
+ var rates = { costCpuPerHour: 0, costMemGbPerHour: 0 };
122
+ var selected = null;
123
+ var liveTimer = null;
124
+ var apiKey = localStorage.getItem("hotcell_api_key") || localStorage.getItem("sbx_api_key") || "";
125
+
126
+ function authHeaders(h) {
127
+ h = h || {};
128
+ if (apiKey) h["authorization"] = "Bearer " + apiKey;
129
+ return h;
130
+ }
131
+ function promptKey() {
132
+ var k = window.prompt("This daemon requires an API key (SBX_API_KEY):", apiKey);
133
+ if (k != null) { apiKey = k; localStorage.setItem("hotcell_api_key", k); }
134
+ return k;
135
+ }
136
+ function api(path, opts) {
137
+ opts = opts || {};
138
+ opts.headers = authHeaders(opts.headers);
139
+ return fetch(path, opts).then(function (r) {
140
+ if (r.status === 401) {
141
+ if (promptKey()) return api(path, opts);
142
+ throw new Error("401: API key required");
143
+ }
144
+ if (!r.ok) return r.text().then(function (t) { throw new Error(r.status + ": " + t); });
145
+ return r.status === 204 ? null : r.json();
146
+ });
147
+ }
148
+ function showErr(e) {
149
+ var el = document.getElementById("err");
150
+ el.style.display = "block"; el.textContent = String(e && e.message ? e.message : e);
151
+ }
152
+ function clearErr() { document.getElementById("err").style.display = "none"; }
153
+
154
+ function computeCost(u) {
155
+ if (!u) return 0;
156
+ var cpu = (u.cpuSeconds / 3600) * rates.costCpuPerHour;
157
+ var mem = (u.memByteSeconds / 1e9 / 3600) * rates.costMemGbPerHour;
158
+ var egress = ((u.egressBytes || 0) / 1e9) * (rates.costEgressPerGb || 0);
159
+ var provider = u.providerCost || 0; // provider-reported LLM cost (USD)
160
+ return cpu + mem + egress + provider;
161
+ }
162
+ function money(n) { return "$" + n.toFixed(6); }
163
+ function ago(iso) {
164
+ if (!iso) return "—";
165
+ var s = Math.max(0, (Date.now() - new Date(iso).getTime()) / 1000);
166
+ if (s < 60) return Math.floor(s) + "s ago";
167
+ if (s < 3600) return Math.floor(s / 60) + "m ago";
168
+ if (s < 86400) return Math.floor(s / 3600) + "h ago";
169
+ return Math.floor(s / 86400) + "d ago";
170
+ }
171
+ function mb(b) { return (b / 1e6).toFixed(1) + " MB"; }
172
+ function sparkline(values, color) {
173
+ if (!values || values.length < 2) return "<span class=\"spark-empty\">—</span>";
174
+ var w = 120, h = 28, n = values.length;
175
+ var max = Math.max.apply(null, values);
176
+ var min = Math.min.apply(null, values);
177
+ var range = max - min || 1;
178
+ var pts = values.map(function (v, i) {
179
+ var x = (i / (n - 1)) * w;
180
+ var y = h - ((v - min) / range) * (h - 4) - 2;
181
+ return x.toFixed(1) + "," + y.toFixed(1);
182
+ }).join(" ");
183
+ return "<svg class=\"spark\" viewBox=\"0 0 " + w + " " + h + "\" width=\"" + w +
184
+ "\" height=\"" + h + "\" preserveAspectRatio=\"none\"><polyline fill=\"none\" stroke=\"" +
185
+ color + "\" stroke-width=\"1.5\" points=\"" + pts + "\" /></svg>";
186
+ }
187
+ function esc(s) { return String(s).replace(/[&<>"]/g, function (c) {
188
+ return { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" }[c]; }); }
189
+
190
+ function loadInfo() {
191
+ // /info is unauthenticated, so we can read whether auth is on and prompt for
192
+ // the key before any authenticated call.
193
+ return api("/info").then(function (i) {
194
+ if (i.auth && !apiKey) promptKey();
195
+ applyInfo(i);
196
+ });
197
+ }
198
+ function applyInfo(i) {
199
+ if (!i) return;
200
+ rates = i;
201
+ document.getElementById("info").textContent =
202
+ "driver=" + i.driver + " · image=" + i.defaultImage + " · proxy=:" + i.proxyPort +
203
+ (i.auth ? " · auth=on" : "") + (i.otlp ? " · otlp=on" : "");
204
+ document.getElementById("newImage").placeholder = "image (default " + i.defaultImage + ")";
205
+ // Show the daemon defaults as placeholders so blank inputs are understood.
206
+ document.getElementById("newMem").placeholder = "mem MB" + (i.defaultMemoryMb ? " (" + i.defaultMemoryMb + ")" : "");
207
+ document.getElementById("newCpus").placeholder = "cpus" + (i.defaultCpus ? " (" + i.defaultCpus + ")" : "");
208
+ document.getElementById("newPids").placeholder = "pids" + (i.defaultPidsLimit ? " (" + i.defaultPidsLimit + ")" : "");
209
+ }
210
+
211
+ function loadCapacity() {
212
+ api("/capacity").then(function (c) {
213
+ var el = document.getElementById("cap");
214
+ if (!c || !c.memory || !c.memory.budgetMb) { el.textContent = ""; return; }
215
+ var gb = function (mb) { return (mb / 1024).toFixed(1); };
216
+ el.innerHTML = "mem <b>" + gb(c.memory.committedMb) + "/" + gb(c.memory.budgetMb) +
217
+ " GB</b> · ~" + c.fits + " more" + (c.enforced ? "" : " (admission off)");
218
+ }).catch(function () {});
219
+ }
220
+
221
+ function refresh() {
222
+ loadCapacity();
223
+ api("/sandboxes").then(function (data) {
224
+ clearErr();
225
+ var list = data.sandboxes || [];
226
+ var total = 0;
227
+ var html = "";
228
+ list.forEach(function (s) {
229
+ var cost = computeCost(s.usage);
230
+ total += cost;
231
+ var sel = s.id === selected ? " class=\"sel\"" : "";
232
+ html += "<tr data-id=\"" + s.id + "\"" + sel + ">";
233
+ html += "<td class=\"id\">" + esc(s.id) + "</td>";
234
+ html += "<td><span class=\"badge " + s.status + "\">" + s.status + "</span></td>";
235
+ html += "<td>" + esc(s.image) + "</td>";
236
+ html += "<td>" + ago(s.createdAt) + "</td>";
237
+ html += "<td>" + ago(s.lastActivityAt) + "</td>";
238
+ html += "<td class=\"num\">" + (s.usage ? s.usage.cpuSeconds.toFixed(1) : "0") + "</td>";
239
+ html += "<td class=\"num\">" + (s.usage ? (s.usage.memByteSeconds / 1e9).toFixed(1) : "0") + "</td>";
240
+ html += "<td class=\"num\">" + money(cost) + "</td>";
241
+ html += "<td class=\"actions\">";
242
+ if (s.status === "running" || s.status === "paused") {
243
+ html += "<button class=\"secondary\" data-act=\"stop\" data-id=\"" + s.id + "\">Stop</button>";
244
+ } else {
245
+ html += "<button class=\"secondary\" data-act=\"start\" data-id=\"" + s.id + "\">Start</button>";
246
+ }
247
+ html += "<button class=\"danger\" data-act=\"rm\" data-id=\"" + s.id + "\">Destroy</button>";
248
+ html += "</td></tr>";
249
+ });
250
+ var rows = document.getElementById("rows");
251
+ rows.innerHTML = html ||
252
+ "<tr><td colspan=\"9\" class=\"empty\">No sandboxes. Create one above.</td></tr>";
253
+ document.getElementById("totalCost").textContent = money(total);
254
+ if (selected && !list.some(function (s) { return s.id === selected; })) {
255
+ selected = null; renderDetail();
256
+ }
257
+ }).catch(showErr);
258
+ }
259
+
260
+ function act(action, id) {
261
+ var p;
262
+ if (action === "stop") p = api("/sandboxes/" + id + "/stop", { method: "POST" });
263
+ else if (action === "start") p = api("/sandboxes/" + id + "/start", { method: "POST" });
264
+ else if (action === "rm") {
265
+ if (!confirm("Destroy " + id + "? Its workspace volume is deleted too.")) return;
266
+ p = api("/sandboxes/" + id, { method: "DELETE" });
267
+ }
268
+ p.then(refresh).catch(showErr);
269
+ }
270
+
271
+ function createSandbox() {
272
+ var body = {};
273
+ var img = document.getElementById("newImage").value.trim();
274
+ var sleep = document.getElementById("newSleep").value.trim();
275
+ var memv = document.getElementById("newMem").value.trim();
276
+ var cpusv = document.getElementById("newCpus").value.trim();
277
+ var pidsv = document.getElementById("newPids").value.trim();
278
+ if (img) body.image = img;
279
+ if (sleep) body.sleepAfter = Number(sleep);
280
+ if (memv) body.memoryMb = Number(memv);
281
+ if (cpusv) body.cpus = Number(cpusv);
282
+ if (pidsv) body.pidsLimit = Number(pidsv);
283
+ api("/sandboxes", {
284
+ method: "POST", headers: { "content-type": "application/json" },
285
+ body: JSON.stringify(body),
286
+ }).then(function () {
287
+ ["newImage", "newSleep", "newMem", "newCpus", "newPids"].forEach(function (k) {
288
+ document.getElementById(k).value = "";
289
+ });
290
+ refresh();
291
+ }).catch(showErr);
292
+ }
293
+
294
+ function select(id) {
295
+ selected = (selected === id) ? null : id;
296
+ refresh();
297
+ renderDetail();
298
+ }
299
+
300
+ function renderDetail() {
301
+ if (liveTimer) { clearInterval(liveTimer); liveTimer = null; }
302
+ var el = document.getElementById("detail");
303
+ if (!selected) { el.innerHTML = ""; return; }
304
+ var id = selected;
305
+ function paint() {
306
+ Promise.all([
307
+ api("/sandboxes/" + id + "/metrics"),
308
+ api("/sandboxes/" + id + "/expose").catch(function () { return { exposed: [] }; }),
309
+ api("/sandboxes/" + id + "/metrics/history").catch(function () { return { samples: [] }; }),
310
+ api("/sandboxes/" + id).catch(function () { return { limits: {} }; }),
311
+ ]).then(function (res) {
312
+ if (selected !== id) return;
313
+ var m = res[0], ex = res[1].exposed || [], hist = res[2].samples || [];
314
+ var limits = (res[3] && res[3].limits) || {};
315
+ var live = m.live;
316
+ var h = "<div class=\"detail\"><h2>" + esc(id) +
317
+ " <span class=\"badge " + m.status + "\">" + m.status + "</span>" +
318
+ "<button class=\"secondary termbtn\" data-term=\"" + id + "\">Terminal</button></h2>";
319
+ if (hist.length >= 2) {
320
+ h += "<div class=\"sparks\">";
321
+ h += "<div class=\"sparkbox\"><div class=\"k\">CPU %</div>" +
322
+ sparkline(hist.map(function (s) { return s.cpuPercent; }), "#2f81f7") + "</div>";
323
+ h += "<div class=\"sparkbox\"><div class=\"k\">Memory</div>" +
324
+ sparkline(hist.map(function (s) { return s.memBytes; }), "#3fb950") + "</div>";
325
+ h += "</div>";
326
+ }
327
+ h += "<div class=\"grid\">";
328
+ h += statCard("CPU", live ? live.cpuPercent.toFixed(1) + "%" : "—");
329
+ h += statCard("CPU limit", limits.cpus ? limits.cpus + " cpu" : "∞");
330
+ h += statCard("Memory", live ? mb(live.memBytes) : "—");
331
+ h += statCard("Mem limit", limits.memoryMb ? limits.memoryMb + " MB" : "∞");
332
+ h += statCard("PIDs", live ? String(live.pids) : "—");
333
+ h += statCard("PID limit", limits.pidsLimit ? String(limits.pidsLimit) : "∞");
334
+ h += statCard("Net in", live ? mb(live.netRxBytes) : "—");
335
+ h += statCard("Net out", live ? mb(live.netTxBytes) : "—");
336
+ h += statCard("CPU total", m.usage.cpuSeconds.toFixed(1) + " vCPU-s");
337
+ h += statCard("Mem total", (m.usage.memByteSeconds / 1e9).toFixed(1) + " GB-s");
338
+ h += statCard("Egress", mb(m.usage.egressBytes || 0));
339
+ h += statCard("Cost", money(m.cost.total));
340
+ if (m.usage.providerCalls) {
341
+ h += statCard("LLM calls", m.usage.providerCalls);
342
+ h += statCard("LLM tokens",
343
+ (m.usage.providerTokensIn || 0) + " in / " + (m.usage.providerTokensOut || 0) + " out");
344
+ h += statCard("LLM cost", money(m.usage.providerCost || 0));
345
+ }
346
+ h += "</div>";
347
+ if (ex.length) {
348
+ h += "<div class=\"ports\" style=\"margin-top:12px\"><b>Preview:</b> ";
349
+ ex.forEach(function (p) {
350
+ h += "<a href=\"" + esc(p.url) + "\" target=\"_blank\">:" + p.port + " ↗</a>";
351
+ });
352
+ h += "</div>";
353
+ }
354
+ h += "</div>";
355
+ el.innerHTML = h;
356
+ }).catch(function (e) { if (selected === id) showErr(e); });
357
+ }
358
+ paint();
359
+ liveTimer = setInterval(paint, 2500);
360
+ }
361
+ function statCard(k, v) {
362
+ return "<div class=\"stat\"><div class=\"k\">" + k + "</div><div class=\"v\">" + v + "</div></div>";
363
+ }
364
+
365
+ // --- live terminal (xterm.js over WebSocket) -------------------------------
366
+ // xterm is loaded lazily from a CDN only when a terminal is first opened, so the
367
+ // rest of the dashboard stays dependency-free and works offline on loopback.
368
+ var XTERM_VER = "5.5.0", FIT_VER = "0.10.0";
369
+ function loadScript(src) {
370
+ return new Promise(function (res, rej) {
371
+ var s = document.createElement("script");
372
+ s.src = src; s.onload = res;
373
+ s.onerror = function () { rej(new Error("failed to load " + src)); };
374
+ document.head.appendChild(s);
375
+ });
376
+ }
377
+ function loadXterm() {
378
+ if (window.Terminal && window.FitAddon) return Promise.resolve();
379
+ var css = document.createElement("link");
380
+ css.rel = "stylesheet";
381
+ css.href = "https://cdn.jsdelivr.net/npm/@xterm/xterm@" + XTERM_VER + "/css/xterm.min.css";
382
+ document.head.appendChild(css);
383
+ return loadScript("https://cdn.jsdelivr.net/npm/@xterm/xterm@" + XTERM_VER + "/lib/xterm.min.js")
384
+ .then(function () {
385
+ return loadScript("https://cdn.jsdelivr.net/npm/@xterm/addon-fit@" + FIT_VER + "/lib/addon-fit.min.js");
386
+ });
387
+ }
388
+ function closeTerm() {
389
+ var host = document.getElementById("terminal");
390
+ if (host._ws) { try { host._ws.close(); } catch (e) {} host._ws = null; }
391
+ if (host._term) { try { host._term.dispose(); } catch (e) {} host._term = null; }
392
+ if (host._onResize) { window.removeEventListener("resize", host._onResize); host._onResize = null; }
393
+ host.innerHTML = ""; host.dataset.id = "";
394
+ }
395
+ function openTerm(id) {
396
+ var host = document.getElementById("terminal");
397
+ if (host.dataset.id === id) { closeTerm(); return; } // toggle off
398
+ closeTerm();
399
+ host.dataset.id = id;
400
+ host.innerHTML =
401
+ "<div class=\"termbar\"><span>terminal — " + esc(id) + "</span><span class=\"spacer\"></span>" +
402
+ "<button class=\"secondary\" id=\"termClose\">Close</button></div><div id=\"termMount\"></div>";
403
+ document.getElementById("termClose").addEventListener("click", closeTerm);
404
+ loadXterm().then(function () {
405
+ if (host.dataset.id !== id) return; // closed while loading
406
+ connectTerm(id, host);
407
+ }).catch(showErr);
408
+ }
409
+ function connectTerm(id, host) {
410
+ var term = new window.Terminal({ fontSize: 13, cursorBlink: true,
411
+ theme: { background: "#0d1117", foreground: "#e6edf3" } });
412
+ var fit = new window.FitAddon.FitAddon();
413
+ term.loadAddon(fit);
414
+ term.open(document.getElementById("termMount"));
415
+ try { fit.fit(); } catch (e) {}
416
+ var wsProto = location.protocol === "https:" ? "wss:" : "ws:";
417
+ var url = wsProto + "//" + location.host + "/sandboxes/" + id + "/terminal" +
418
+ "?cols=" + term.cols + "&rows=" + term.rows;
419
+ if (apiKey) url += "&key=" + encodeURIComponent(apiKey);
420
+ var ws = new WebSocket(url);
421
+ ws.binaryType = "arraybuffer";
422
+ host._ws = ws; host._term = term;
423
+ var enc = new TextEncoder();
424
+ ws.onmessage = function (e) {
425
+ if (typeof e.data === "string") term.write(e.data);
426
+ else term.write(new Uint8Array(e.data));
427
+ };
428
+ ws.onclose = function () { try { term.write("\r\n\x1b[90m[connection closed]\x1b[0m\r\n"); } catch (x) {} };
429
+ ws.onerror = function () { showErr("terminal websocket error"); };
430
+ term.onData(function (d) { if (ws.readyState === 1) ws.send(enc.encode(d)); });
431
+ term.onResize(function (s) {
432
+ if (ws.readyState === 1) ws.send(JSON.stringify({ type: "resize", cols: s.cols, rows: s.rows }));
433
+ });
434
+ host._onResize = function () { try { fit.fit(); } catch (e) {} };
435
+ window.addEventListener("resize", host._onResize);
436
+ }
437
+ document.getElementById("detail").addEventListener("click", function (e) {
438
+ var b = e.target.closest("button[data-term]");
439
+ if (b) { e.stopPropagation(); openTerm(b.dataset.term); }
440
+ });
441
+
442
+ document.getElementById("rows").addEventListener("click", function (e) {
443
+ var btn = e.target.closest("button");
444
+ if (btn && btn.dataset.act) { e.stopPropagation(); act(btn.dataset.act, btn.dataset.id); return; }
445
+ var tr = e.target.closest("tr");
446
+ if (tr && tr.dataset.id) select(tr.dataset.id);
447
+ });
448
+ document.getElementById("newBtn").addEventListener("click", createSandbox);
449
+ document.getElementById("refreshBtn").addEventListener("click", refresh);
450
+
451
+ loadInfo().then(refresh).catch(showErr);
452
+ setInterval(refresh, 3000);
453
+ </script>
454
+ </body>
455
+ </html>`;
456
+ //# sourceMappingURL=dashboard.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dashboard.js","sourceRoot":"","sources":["../../src/web/dashboard.ts"],"names":[],"mappings":"AAAA,iFAAiF;AACjF,gFAAgF;AAChF,iFAAiF;AACjF,iFAAiF;AACjF,0DAA0D;AAC1D,EAAE;AACF,+EAA+E;AAC/E,+EAA+E;AAC/E,kDAAkD;AAElD,MAAM,CAAC,MAAM,cAAc,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;QA6bhC,CAAC"}
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@hotcell/daemon",
3
+ "version": "0.1.0",
4
+ "description": "hotcell sandbox daemon: containers + Firecracker/Apple-VZ microVMs, egress credential gateway",
5
+ "license": "Apache-2.0",
6
+ "type": "module",
7
+ "bin": {
8
+ "hotcelld": "dist/index.js"
9
+ },
10
+ "main": "dist/index.js",
11
+ "scripts": {
12
+ "build": "tsc -p tsconfig.json",
13
+ "dev": "node --import tsx src/index.ts",
14
+ "start": "node dist/index.js"
15
+ },
16
+ "dependencies": {
17
+ "dockerode": "^4.0.2"
18
+ },
19
+ "devDependencies": {
20
+ "@types/dockerode": "^3.3.31",
21
+ "@types/node": "^22.5.0",
22
+ "tsx": "^4.16.0",
23
+ "typescript": "^5.5.0",
24
+ "@hotcell/sdk": "^0.1.0"
25
+ },
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "git+https://github.com/sinameraji/hotcell.git"
29
+ },
30
+ "homepage": "https://hotcell-bw3.pages.dev",
31
+ "bugs": {
32
+ "url": "https://github.com/sinameraji/hotcell/issues"
33
+ },
34
+ "keywords": [
35
+ "sandbox",
36
+ "ai-agents",
37
+ "firecracker",
38
+ "microvm",
39
+ "apple-virtualization",
40
+ "code-execution",
41
+ "llm-gateway",
42
+ "self-hosted"
43
+ ],
44
+ "files": [
45
+ "dist"
46
+ ],
47
+ "engines": {
48
+ "node": ">=22"
49
+ },
50
+ "publishConfig": {
51
+ "access": "public"
52
+ }
53
+ }