Theflame47 commited on
Commit
0bbbc1e
·
verified ·
1 Parent(s): 9dd3a51

Update Deployment_UI.py

Browse files
Files changed (1) hide show
  1. Deployment_UI.py +227 -221
Deployment_UI.py CHANGED
@@ -1,4 +1,4 @@
1
- # Deployment_UI.py — UI-only (binds to /api/* provided by Deployment_UI_BE.py)
2
  from fastapi import APIRouter
3
  from fastapi.responses import HTMLResponse
4
 
@@ -6,239 +6,245 @@ router = APIRouter()
6
 
7
  @router.get("/Deployment_UI", response_class=HTMLResponse)
8
  def deployment_ui_page():
9
- html_head = """
10
  <!doctype html>
11
- <html><head><meta charset="utf-8"/><title>Deployment UI</title>
12
- <style>
13
- body { font-family: system-ui, sans-serif; margin: 24px; display: flex; flex-direction: column; height: 90vh; }
14
- a.button { display:inline-block; padding:8px 14px; margin-bottom:10px; border:1px solid #ccc; border-radius:6px; text-decoration:none; color:#000; background:#f7f7f7; }
15
- a.button:hover { background:#eee; }
16
- #chat-window { flex:1; border:1px solid #ccc; border-radius:8px; padding:12px; overflow-y:auto; background:#fafafa; margin-bottom:10px; }
17
- #input-area { display:flex; gap:8px; margin-bottom:10px; }
18
- #user-input { flex:1; padding:10px; border:1px solid #ccc; border-radius:8px; }
19
- #send-btn { padding:10px 16px; border:none; border-radius:8px; background:#0078d7; color:#fff; cursor:pointer; }
20
- #send-btn:hover { background:#005fa3; }
21
- #control-bar { display:flex; gap:8px; margin:0 0 10px; }
22
- #start-btn, #stop-btn, #endall-btn { padding:8px 14px; border:1px solid #ccc; border-radius:8px; background:#f7f7f7; cursor:pointer; }
23
- #start-btn:hover, #stop-btn:hover, #endall-btn:hover { background:#eee; }
24
- #log-toggle { cursor:pointer; font-size:14px; color:#0078d7; text-decoration:underline; margin-bottom:6px; align-self:flex-start; }
25
- #logs { border:1px dashed #bbb; border-radius:8px; padding:10px; font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; font-size:12px; background:#fff; max-height:220px; overflow-y:auto; transition:max-height .25s ease, opacity .25s ease; opacity:1; }
26
- #logs.collapsed { max-height:0; padding:0; opacity:0; overflow:hidden; border:none; }
27
- .log-line { margin:0 0 6px; white-space:pre-wrap; }
28
- .log-raw { color:#333; } .log-ok { color:#2d7c2d; } .log-err { color:#9d1c1c; }
29
- #results { border:1px solid #ddd; border-radius:8px; padding:10px; background:#fff; margin-top:10px; }
30
- .result { margin:8px 0; }
31
- .thumb { max-width: 420px; border:1px solid #ccc; border-radius:6px; }
32
- .meta { font-size:12px; color:#555; margin-top:4px; }
33
- .download { display:inline-block; margin-top:6px; }
34
- /* loading mask */
35
- #boot-mask{position:fixed;inset:0;background:rgba(255,255,255,.85);display:none;
36
- align-items:center;justify-content:center;flex-direction:column;z-index:9999}
37
- #boot-msg{margin-top:12px;color:#000;font-weight:600}
38
- .spinner{width:36px;height:36px;border:3px solid #ddd;border-top-color:#0078d7;border-radius:50%;
39
- animation:spin 1s linear infinite}
40
- @keyframes spin{to{transform:rotate(360deg)}}
41
- </style>
42
  </head>
43
  <body>
44
- <a href="/trythis" class="button">← Back</a>
45
- <div id="chat-window"></div>
46
- <div id="input-area">
47
- <input type="text" id="user-input" placeholder="Describe an image to generate…" />
48
- <button id="send-btn">Send</button>
49
- </div>
50
- <div id="control-bar">
51
- <button id="start-btn">Create inst.</button>
52
- <button id="stop-btn">Stop</button>
53
- <button id="endall-btn">End All</button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
  </div>
55
- <div id="log-toggle">Hide Logs</div>
56
- <div id="logs" aria-live="polite"></div>
57
- <div id="results"></div>
58
- <div id="boot-mask" aria-live="polite" role="status">
59
- <div class="spinner"></div>
60
- <div id="boot-msg">Starting…</div>
 
 
61
  </div>
 
62
  <script>
63
- """
64
- html_tail = """
65
- const logs = document.getElementById('logs');
66
- const toggleBtn = document.getElementById('log-toggle');
67
- const startBtn = document.getElementById('start-btn');
68
- const stopBtn = document.getElementById('stop-btn');
69
- const endAllBtn = document.getElementById('endall-btn');
70
- const sendBtn = document.getElementById('send-btn');
71
- const userInput = document.getElementById('user-input');
72
- const results = document.getElementById('results');
73
- const chat = document.getElementById('chat-window');
74
- const bootMask = document.getElementById('boot-mask');
75
- const bootMsg = document.getElementById('boot-msg');
76
- let running = false, ready = false;
77
- let MODEL_BASE = null, PREDICT_ROUTE = null;
78
-
79
- // NEW: auto-ingest blob from FE query param on page load
80
- (async () => {
81
- try {
82
- const u = new URL(window.location.href);
83
- const blobUrl = u.searchParams.get('blob_url') || '/modelblob.json';
84
- const r = await fetch(`/api/ingest/from_landing?blob_url=${encodeURIComponent(blobUrl)}`, { method: 'POST' });
85
- const t = await r.text();
86
- console.log('ingest-from-landing', r.status, t.slice(0,200));
87
- } catch (e) {
88
- console.warn('ingest bootstrap failed:', e);
89
- }
90
- })();
91
-
92
- function showBoot(msg){ bootMsg.textContent = msg || 'Starting…'; bootMask.style.display = 'flex'; }
93
- function setBoot(msg){ bootMsg.textContent = msg || 'Working…'; }
94
- function hideBoot(){ bootMask.style.display = 'none'; }
95
- function log(msg, cls='log-raw') {
96
- const line = document.createElement('div');
97
- line.className = 'log-line ' + cls;
98
- line.textContent = String(msg);
99
- logs.appendChild(line);
100
- logs.scrollTop = logs.scrollHeight;
101
- }
102
- toggleBtn.addEventListener('click', () => {
103
- const collapsed = logs.classList.toggle('collapsed');
104
- toggleBtn.textContent = collapsed ? "Show Logs" : "Hide Logs";
105
- });
106
- async function post(path, payload) {
107
- const r = await fetch(path, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(payload || {}) });
108
- const text = await r.text();
109
- log(text, r.ok ? 'log-ok' : 'log-err');
110
- try { return { ok: r.ok, json: JSON.parse(text) }; } catch { return { ok: r.ok, json: { _raw: text } }; }
111
- }
112
- async function del(path) {
113
- const r = await fetch(path, { method: 'DELETE' });
114
- const text = await r.text();
115
- log(text, r.ok ? 'log-ok' : 'log-err');
116
- try { return { ok: r.ok, json: JSON.parse(text) }; } catch { return { ok: r.ok, json: { _raw: text } }; }
117
- }
118
- // create + wait for true readiness (isReady)
119
- async function begin() {
120
- if (running) return;
121
- running = true; ready = false;
122
- showBoot('Creating instance…');
123
- const create = await post('/api/compute/create_instance');
124
- if (!create.ok) { running = false; hideBoot(); return; }
125
- const ok = await ensureReady(true);
126
- hideBoot();
127
- }
128
- async function stopOnce() {
129
- await del('/api/compute/delete_instance');
130
- hideBoot(); setBoot(''); ready = false; running = false;
131
- }
132
- async function endAll() {
133
- await stopOnce();
134
- MODEL_BASE = null; PREDICT_ROUTE = null;
135
  }
136
- function appendResult(job_id, b64, timings) {
137
- const div = document.createElement('div'); div.className = 'result';
138
- const img = document.createElement('img'); img.className = 'thumb'; img.src = 'data:image/png;base64,' + b64;
139
- const a = document.createElement('a'); a.className = 'download'; a.textContent = 'Download'; a.href = img.src; a.download = `job_${job_id}.png`;
140
- const meta = document.createElement('div'); meta.className = 'meta'; meta.textContent = `job ${job_id} | ${JSON.stringify(timings || {})}`;
141
- div.append(img, document.createTextNode(' '), a, meta); results.prepend(div);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
142
  }
143
- // chat helpers
144
- function escapeHtml(s){ return String(s).replace(/[&<>"']/g, m => ({ '&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;' }[m])); }
145
- function addBlock(sender, html) {
146
- const wrap = document.createElement('div');
147
- wrap.style.margin = '8px 0';
148
- wrap.innerHTML = `<div class="meta"><strong>${sender}:</strong></div><div>${html}</div>`;
149
- chat.appendChild(wrap);
150
- chat.scrollTop = chat.scrollHeight;
151
- return wrap;
 
 
 
 
152
  }
153
- function addUser(text) { return addBlock('You', `<div>${escapeHtml(text)}</div>`); }
154
- function addModel(text) { return addBlock('Model', `<div>${escapeHtml(text)}</div>`); }
155
- function addModelImg(b64){
156
- const wrap = addBlock('Model', '');
157
- const img = document.createElement('img');
158
- img.className = 'thumb';
159
- img.src = 'data:image/png;base64,' + b64;
160
- wrap.lastElementChild.appendChild(img);
161
- return wrap;
 
 
 
 
162
  }
163
- function addLoader() {
164
- const wrap = addBlock('Model', `<div id="spinner" style="display:inline-block">Loading…</div>`);
165
- return wrap;
 
 
 
 
 
 
 
 
 
 
166
  }
167
- function looksBase64(s){ return /^[A-Za-z0-9+/=\\s]+$/.test(s||'') && String(s||'').length > 100; }
168
- // poll for true readiness provided by BE via cachedState.isReady
169
- async function ensureReady(verbose=false) {
170
- for (let i = 0; i < 60; i++) {
171
- const r = await fetch('/api/compute/wait_instance');
172
- const j = await r.json();
173
- const cs = j.cachedState || {};
174
- const status = (cs.status || '').toUpperCase();
175
- if (verbose) {
176
- if (cs.isReady === true) setBoot('Ready');
177
- else if (status === 'RUNNING' && cs.base) setBoot('Warming model…');
178
- else setBoot(`Status: ${status || '…'}`);
179
- }
180
- if (cs.base && cs.predictRoute && cs.isReady === true) {
181
- MODEL_BASE = cs.base;
182
- PREDICT_ROUTE = cs.predictRoute.startsWith('/') ? cs.predictRoute : `/${cs.predictRoute}`;
183
- log(`PROMPT_ENDPOINT ${MODEL_BASE}${PREDICT_ROUTE}`, 'log-ok');
184
- ready = true;
185
- return true;
186
- }
187
- log(`READY_POLL base=${cs.base ? 'yes' : 'no'} route=${cs.predictRoute || ''} isReady=${cs.isReady === true} status=${status}`, 'log-raw');
188
- await new Promise(res => setTimeout(res, 1000));
189
- }
190
- ready = false;
191
- return false;
192
  }
193
- // backend hop for prompts
194
- async function sendViaBackend(prompt) {
195
- const r = await fetch('/api/middleware/infer', {
196
- method: 'POST',
197
- headers: { 'content-type': 'application/json' },
198
- body: JSON.stringify({ prompt })
199
- });
200
- const text = await r.text();
201
- log(`POST /api/middleware/infer ${r.status}`, r.ok ? 'log-ok' : 'log-err');
202
- try { return { ok: r.ok, json: JSON.parse(text) }; } catch { return { ok: r.ok, json: { _raw: text } }; }
 
 
 
 
203
  }
204
- async function sendMessage() {
205
- const prompt = (userInput.value || '').trim();
206
- if (!prompt) return;
207
- if (!ready) { addModel('Instance not ready yet. Try Start, or wait a moment.'); return; }
208
- addUser(prompt);
209
- const loader = addLoader();
210
- userInput.disabled = true;
211
- try {
212
- const { ok, json } = await sendViaBackend(prompt);
213
- loader.remove();
214
- if (!ok && json?.error) { addModel(`Error: ${json.error}`); return; }
215
- if (json?.image_b64) {
216
- addModelImg(json.image_b64);
217
- if (json.timings) appendResult(String(Date.now()), json.image_b64, json.timings);
218
- } else if (typeof json?.output === 'string' && looksBase64(json.output)) {
219
- addModelImg(json.output);
220
- } else if (typeof json?.output === 'string') {
221
- addModel(json.output);
222
- } else if (json?._raw) {
223
- addModel(json._raw);
224
- } else {
225
- addModel(JSON.stringify(json || {}, null, 2));
226
- }
227
- } catch (e) {
228
- loader.remove();
229
- addModel(`Error: ${String(e)}`);
230
- } finally {
231
- userInput.disabled = false; userInput.value = '';
232
- userInput.focus();
233
- }
234
  }
235
- document.getElementById('send-btn').addEventListener('click', sendMessage);
236
- document.getElementById('user-input').addEventListener('keypress', (e) => { if (e.key === 'Enter') { e.preventDefault(); sendMessage(); } });
237
- document.getElementById('start-btn').addEventListener('click', begin);
238
- document.getElementById('stop-btn').addEventListener('click', stopOnce);
239
- document.getElementById('endall-btn').addEventListener('click', endAll);
240
- (function init(){})();
241
  </script>
242
- </body></html>
 
243
  """
244
- return HTMLResponse(content=html_head + html_tail)
 
1
+ # Deployment_UI.py — FE page with explicit Blob load/preview + ingest
2
  from fastapi import APIRouter
3
  from fastapi.responses import HTMLResponse
4
 
 
6
 
7
  @router.get("/Deployment_UI", response_class=HTMLResponse)
8
  def deployment_ui_page():
9
+ html = """
10
  <!doctype html>
11
+ <html lang="en">
12
+ <head>
13
+ <meta charset="utf-8"/>
14
+ <title>Deployment UI</title>
15
+ <meta name="viewport" content="width=device-width,initial-scale=1"/>
16
+ <style>
17
+ :root { --gap: 12px; --pad: 12px; --border: 1px solid #ddd; --radius: 10px; }
18
+ body { font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif; margin: 20px; color: #111; }
19
+ h1 { margin: 0 0 6px; font-size: 20px; }
20
+ .row { display: flex; gap: var(--gap); flex-wrap: wrap; }
21
+ .col { flex: 1 1 360px; min-width: 320px; }
22
+ .card { border: var(--border); border-radius: var(--radius); padding: var(--pad); background: #fff; }
23
+ .btn { border: 1px solid #ccc; background: #f7f7f7; padding: 8px 12px; border-radius: 8px; cursor: pointer; }
24
+ .btn:hover { background: #eee; }
25
+ .btn[disabled] { opacity: 0.5; cursor: not-allowed; }
26
+ input[type="text"] { width: 100%; padding: 8px; border: var(--border); border-radius: 8px; }
27
+ pre { white-space: pre-wrap; word-break: break-word; background: #fafafa; border: var(--border); border-radius: 8px; padding: 8px; max-height: 360px; overflow: auto; }
28
+ .mono { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace; }
29
+ .muted { color: #666; }
30
+ .ok { color: #0a7f2e; }
31
+ .err { color: #9b1c1c; }
32
+ .kvs { font-size: 13px; }
33
+ .line { display:flex; gap:8px; align-items:center; margin: 6px 0; }
34
+ .small { font-size: 12px; }
35
+ </style>
 
 
 
 
 
 
36
  </head>
37
  <body>
38
+ <h1>Deployment UI</h1>
39
+ <div class="row">
40
+ <div class="col">
41
+ <div class="card">
42
+ <div class="line">
43
+ <label for="blobUrl"><strong>Blob URL</strong></label>
44
+ <span class="muted small">(defaults to <code class="mono">/modelblob.json</code>)</span>
45
+ </div>
46
+ <input id="blobUrl" type="text" placeholder="/modelblob.json"/>
47
+ <div class="line">
48
+ <button id="btnLoadBlob" class="btn">Load Blob</button>
49
+ <button id="btnIngest" class="btn" disabled>Ingest → BE</button>
50
+ <span id="blobStatus" class="small muted"></span>
51
+ </div>
52
+ <pre id="blobPreview" class="mono muted">Blob not loaded.</pre>
53
+ </div>
54
+ </div>
55
+
56
+ <div class="col">
57
+ <div class="card">
58
+ <div class="line"><strong>Actions</strong></div>
59
+ <div class="line">
60
+ <button id="btnCreate" class="btn" disabled>Create Instance</button>
61
+ <button id="btnStart" class="btn" disabled>Start</button>
62
+ <button id="btnWait" class="btn" disabled>Wait</button>
63
+ </div>
64
+ <div class="line">
65
+ <button id="btnStop" class="btn" disabled>Stop</button>
66
+ <button id="btnEndAll" class="btn" disabled>End All</button>
67
+ </div>
68
+ <div class="kvs mono" id="stateKvs"></div>
69
+ </div>
70
+ </div>
71
  </div>
72
+
73
+ <div class="row" style="margin-top:12px;">
74
+ <div class="col">
75
+ <div class="card">
76
+ <div class="line"><strong>Logs</strong> <span class="muted small">(key responses)</span></div>
77
+ <pre id="logs" class="mono"></pre>
78
+ </div>
79
+ </div>
80
  </div>
81
+
82
  <script>
83
+ const $ = (id) => document.getElementById(id);
84
+ const logsEl = $("logs");
85
+ const blobPreviewEl = $("blobPreview");
86
+ const blobStatusEl = $("blobStatus");
87
+ const stateKvsEl = $("stateKvs");
88
+
89
+ let cachedBlob = null;
90
+ let cachedPodId = null;
91
+
92
+ function logLine(s, cls="") {
93
+ const at = new Date().toLocaleTimeString();
94
+ const line = `[${at}] ${s}\\n`;
95
+ logsEl.textContent += line;
96
+ logsEl.scrollTop = logsEl.scrollHeight;
97
+ }
98
+
99
+ function setActionsEnabled(enabled) {
100
+ $("btnCreate").disabled = !enabled;
101
+ $("btnStart").disabled = !enabled || !cachedPodId;
102
+ $("btnWait").disabled = !enabled || !cachedPodId;
103
+ $("btnStop").disabled = !enabled || !cachedPodId;
104
+ $("btnEndAll").disabled = !enabled || !cachedPodId;
105
+ }
106
+
107
+ function updateStateKvs(obj) {
108
+ const st = obj || {};
109
+ const cs = (st.cachedState || {});
110
+ const kvs = [
111
+ ["podId", cs.podId || ""],
112
+ ["status", cs.status || ""],
113
+ ["ip", cs.ip || ""],
114
+ ["port", cs.port || ""],
115
+ ["predictRoute", cs.predictRoute || ""],
116
+ ["base", cs.base || ""],
117
+ ];
118
+ stateKvsEl.innerHTML = kvs.map(([k,v]) => `<div><strong>${k}</strong>: <span class="mono">${String(v)}</span></div>`).join("");
119
+ if (cs.podId) cachedPodId = cs.podId;
120
+ setActionsEnabled(true);
121
+ }
122
+
123
+ async function fetchJSON(url, opts={}) {
124
+ const r = await fetch(url, opts);
125
+ const ct = r.headers.get("content-type") || "";
126
+ if (!ct.includes("application/json")) {
127
+ const t = await r.text();
128
+ throw new Error(`Non-JSON response ${r.status}: ${t.slice(0,200)}`);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
129
  }
130
+ const j = await r.json();
131
+ if (!r.ok) throw new Error(JSON.stringify(j).slice(0,200));
132
+ return j;
133
+ }
134
+
135
+ function pretty(obj) {
136
+ try { return JSON.stringify(obj, null, 2); } catch { return String(obj); }
137
+ }
138
+
139
+ $("btnLoadBlob").addEventListener("click", async () => {
140
+ const url = ($("blobUrl").value || "/modelblob.json").trim();
141
+ blobStatusEl.textContent = "Loading…";
142
+ try {
143
+ const j = await fetchJSON(url, { method: "GET" });
144
+ cachedBlob = j;
145
+ blobPreviewEl.textContent = pretty(j);
146
+ blobPreviewEl.classList.remove("muted");
147
+ blobStatusEl.textContent = "Blob loaded";
148
+ $("btnIngest").disabled = false;
149
+ logLine(`BLOB_LOADED from ${url}`);
150
+ } catch (e) {
151
+ blobStatusEl.textContent = "Load failed";
152
+ blobPreviewEl.textContent = String(e.message || e);
153
+ blobPreviewEl.classList.remove("muted");
154
+ $("btnIngest").disabled = true;
155
+ logLine(`BLOB_LOAD_ERROR ${e.message || e}`, "err");
156
  }
157
+ });
158
+
159
+ $("btnIngest").addEventListener("click", async () => {
160
+ const url = ($("blobUrl").value || "/modelblob.json").trim();
161
+ blobStatusEl.textContent = "Ingesting…";
162
+ try {
163
+ const j = await fetchJSON(`/api/ingest/from_landing?blob_url=${encodeURIComponent(url)}`, { method: "POST" });
164
+ blobStatusEl.textContent = `Ingested (${j.source || url})`;
165
+ $("btnCreate").disabled = false;
166
+ logLine(`BLOB_INGEST_OK source=${j.source || url}`);
167
+ } catch (e) {
168
+ blobStatusEl.textContent = "Ingest failed";
169
+ logLine(`BLOB_INGEST_ERROR ${e.message || e}`, "err");
170
  }
171
+ });
172
+
173
+ $("btnCreate").addEventListener("click", async () => {
174
+ setActionsEnabled(false);
175
+ logLine("CREATE /api/compute/create_instance");
176
+ try {
177
+ const j = await fetchJSON("/api/compute/create_instance", { method: "POST", headers: { "Content-Type": "application/json" }, body: "{}" });
178
+ logLine("CREATE_OK " + (j.id || j.podId || JSON.stringify(j).slice(0,120)));
179
+ await refreshStatus();
180
+ } catch (e) {
181
+ logLine("CREATE_ERROR " + (e.message || e), "err");
182
+ } finally {
183
+ setActionsEnabled(true);
184
  }
185
+ });
186
+
187
+ $("btnStart").addEventListener("click", async () => {
188
+ if (!cachedPodId) return;
189
+ setActionsEnabled(false);
190
+ logLine(`START → /api/compute/pods/${cachedPodId}/start`);
191
+ try {
192
+ const j = await fetchJSON(`/api/compute/pods/${cachedPodId}/start`, { method: "POST" });
193
+ logLine("START_OK " + JSON.stringify(j).slice(0,120));
194
+ } catch (e) {
195
+ logLine("START_ERROR " + (e.message || e), "err");
196
+ } finally {
197
+ setActionsEnabled(true);
198
  }
199
+ });
200
+
201
+ $("btnWait").addEventListener("click", async () => {
202
+ await refreshStatus(true);
203
+ });
204
+
205
+ $("btnStop").addEventListener("click", async () => {
206
+ setActionsEnabled(false);
207
+ logLine("STOP → /api/compute/delete_instance");
208
+ try {
209
+ const j = await fetchJSON("/api/compute/delete_instance", { method: "DELETE" });
210
+ logLine("STOP_OK " + JSON.stringify(j).slice(0,120));
211
+ } catch (e) {
212
+ logLine("STOP_ERROR " + (e.message || e), "err");
213
+ } finally {
214
+ await refreshStatus();
215
+ setActionsEnabled(true);
 
 
 
 
 
 
 
 
216
  }
217
+ });
218
+
219
+ $("btnEndAll").addEventListener("click", async () => {
220
+ if (!cachedPodId) return;
221
+ setActionsEnabled(false);
222
+ logLine(`END_ALL /api/compute/end_all`);
223
+ try {
224
+ const j = await fetchJSON("/api/compute/end_all", { method: "DELETE" });
225
+ logLine("END_ALL_OK " + JSON.stringify(j).slice(0,120));
226
+ } catch (e) {
227
+ logLine("END_ALL_ERROR " + (e.message || e), "err");
228
+ } finally {
229
+ await refreshStatus();
230
+ setActionsEnabled(true);
231
  }
232
+ });
233
+
234
+ async function refreshStatus(verbose=false) {
235
+ try {
236
+ const j = await fetchJSON("/api/compute/pods/placeholder?pod_id=", { method: "GET" });
237
+ if (verbose) logLine("STATUS " + JSON.stringify(j).slice(0,180));
238
+ updateStateKvs(j);
239
+ } catch (e) {
240
+ logLine("STATUS_ERROR " + (e.message || e), "err");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
241
  }
242
+ }
243
+
244
+ // On load: try to show current state (if any); do NOT auto-ingest.
245
+ refreshStatus();
 
 
246
  </script>
247
+ </body>
248
+ </html>
249
  """
250
+ return HTMLResponse(html)