@firstpick/pi-package-webui 0.2.0 → 0.2.2

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.
package/public/app.js CHANGED
@@ -1,13 +1,16 @@
1
1
  const $ = (selector) => document.querySelector(selector);
2
2
 
3
3
  const elements = {
4
- sessionLine: $("#sessionLine"),
4
+ webuiVersionBadge: $("#webuiVersionBadge"),
5
+ webuiDevBadge: $("#webuiDevBadge"),
5
6
  tabBar: $("#tabBar"),
6
7
  terminalTabsToggleButton: $("#terminalTabsToggleButton"),
7
8
  newTabButton: $("#newTabButton"),
8
9
  closeAllTabsButton: $("#closeAllTabsButton"),
9
10
  statusBar: $("#statusBar"),
10
11
  serverOfflinePanel: $("#serverOfflinePanel"),
12
+ serverRestartPanel: $("#serverRestartPanel"),
13
+ serverRestartMessage: $("#serverRestartMessage"),
11
14
  serverOfflineCommand: $("#serverOfflineCommand"),
12
15
  serverOfflineSlashCommand: $("#serverOfflineSlashCommand"),
13
16
  copyServerCommandButton: $("#copyServerCommandButton"),
@@ -60,7 +63,9 @@ const elements = {
60
63
  backgroundStatus: $("#backgroundStatus"),
61
64
  networkStatus: $("#networkStatus"),
62
65
  openNetworkButton: $("#openNetworkButton"),
63
- stopServerButton: $("#stopServerButton"),
66
+ serverActionSelect: $("#serverActionSelect"),
67
+ runServerActionButton: $("#runServerActionButton"),
68
+ serverActionStatus: $("#serverActionStatus"),
64
69
  agentDoneNotificationsToggle: $("#agentDoneNotificationsToggle"),
65
70
  agentDoneNotificationsStatus: $("#agentDoneNotificationsStatus"),
66
71
  optionalFeaturesBox: $("#optionalFeaturesBox"),
@@ -150,12 +155,15 @@ let pathSuggestAbortController = null;
150
155
  let latestStats = null;
151
156
  let latestWorkspace = null;
152
157
  let latestNetwork = null;
158
+ let webuiVersion = "";
159
+ let webuiDevServer = false;
153
160
  let latestCodexUsage = null;
154
161
  let codexUsageError = null;
155
162
  let codexUsageLoading = false;
156
163
  let refreshCodexUsageTimer = null;
157
164
  let codexUsageRenderTimer = null;
158
165
  let backendOffline = false;
166
+ let serverRestartInProgress = false;
159
167
  let backendOfflineNoticeShown = false;
160
168
  let latestMessages = [];
161
169
  let promptHistoryByTab = new Map();
@@ -263,6 +271,7 @@ const mobileViewMedia = window.matchMedia?.(MOBILE_VIEW_QUERY) || null;
263
271
  const statusEntries = new Map();
264
272
  const widgets = new Map();
265
273
  const todoProgressWidgetExpandedByTab = new Map();
274
+ const releaseNpmOutputExpandedByTab = new Map();
266
275
  const liveToolRuns = new Map();
267
276
  const liveToolCards = new Map();
268
277
  const liveToolRenderQueue = new Map();
@@ -899,13 +908,22 @@ function renderServerOfflinePanel() {
899
908
  if (elements.serverOfflineSlashCommand) elements.serverOfflineSlashCommand.textContent = serverStartSlashCommandText();
900
909
  }
901
910
 
911
+ function setServerRestartOverlay(active, message = "Waiting for the server to come back…") {
912
+ serverRestartInProgress = !!active;
913
+ document.body.classList.toggle("server-restarting", serverRestartInProgress);
914
+ if (elements.serverRestartPanel) elements.serverRestartPanel.hidden = !serverRestartInProgress;
915
+ if (elements.serverRestartMessage) elements.serverRestartMessage.textContent = message;
916
+ if (serverRestartInProgress && elements.serverOfflinePanel) elements.serverOfflinePanel.hidden = true;
917
+ }
918
+
902
919
  function setBackendOffline(offline, error) {
903
920
  backendOffline = !!offline;
904
- document.body.classList.toggle("server-offline", backendOffline);
905
- if (elements.serverOfflinePanel) elements.serverOfflinePanel.hidden = !backendOffline;
921
+ const showOfflinePanel = backendOffline && !serverRestartInProgress;
922
+ document.body.classList.toggle("server-offline", showOfflinePanel);
923
+ if (elements.serverOfflinePanel) elements.serverOfflinePanel.hidden = !showOfflinePanel;
906
924
  renderServerOfflinePanel();
907
925
  if (backendOffline) {
908
- if (!backendOfflineNoticeShown) {
926
+ if (!serverRestartInProgress && !backendOfflineNoticeShown) {
909
927
  backendOfflineNoticeShown = true;
910
928
  addEvent(`Pi Web UI server is offline${error?.message ? `: ${error.message}` : ""}`, "warn");
911
929
  }
@@ -1015,6 +1033,52 @@ async function api(path, { method = "GET", body, tabId = activeTabId, scoped = t
1015
1033
  return data;
1016
1034
  }
1017
1035
 
1036
+ function formatWebuiVersion(version) {
1037
+ const text = String(version || "").trim();
1038
+ if (!text) return "";
1039
+ return text.startsWith("v") ? text : `v${text}`;
1040
+ }
1041
+
1042
+ function isWebuiDevMetadata(data) {
1043
+ return data?.webuiDev === true || String(data?.webuiMode || "").toLowerCase() === "dev";
1044
+ }
1045
+
1046
+ function renderWebuiVersion() {
1047
+ const badge = elements.webuiVersionBadge;
1048
+ if (!badge) return;
1049
+ const label = formatWebuiVersion(webuiVersion);
1050
+ badge.hidden = !label;
1051
+ badge.textContent = label;
1052
+ if (label) badge.title = `Pi Web UI ${label}`;
1053
+ }
1054
+
1055
+ function renderWebuiDevBadge() {
1056
+ const badge = elements.webuiDevBadge;
1057
+ if (!badge) return;
1058
+ badge.hidden = !webuiDevServer;
1059
+ badge.title = "Pi Web UI dev server";
1060
+ }
1061
+
1062
+ function setWebuiVersion(version) {
1063
+ const text = String(version || "").trim();
1064
+ if (text === webuiVersion) return;
1065
+ webuiVersion = text;
1066
+ renderWebuiVersion();
1067
+ }
1068
+
1069
+ function setWebuiDevServer(dev) {
1070
+ const next = !!dev;
1071
+ if (next === webuiDevServer) return;
1072
+ webuiDevServer = next;
1073
+ renderWebuiDevBadge();
1074
+ }
1075
+
1076
+ async function refreshWebuiVersion() {
1077
+ const health = await api("/api/health", { scoped: false });
1078
+ setWebuiVersion(health.webuiVersion);
1079
+ setWebuiDevServer(isWebuiDevMetadata(health));
1080
+ }
1081
+
1018
1082
  function formatBytes(bytes) {
1019
1083
  const value = Number(bytes) || 0;
1020
1084
  if (value < 1024) return `${value} B`;
@@ -2357,7 +2421,6 @@ function resetActiveTabUi() {
2357
2421
  }
2358
2422
  elements.commandsBox.textContent = "Loading…";
2359
2423
  elements.commandsBox.classList.add("muted");
2360
- elements.sessionLine.textContent = activeTab() ? "Connecting…" : "No terminal tabs.";
2361
2424
  renderWidgets();
2362
2425
  renderGitWorkflow();
2363
2426
  renderFooter();
@@ -3899,13 +3962,6 @@ function renderStatus() {
3899
3962
  updateComposerModeButtons();
3900
3963
  const running = state?.isStreaming ? "running" : "idle";
3901
3964
  const compacting = state?.isCompacting ? " · compacting" : "";
3902
- const queue = state?.pendingMessageCount ? ` · queued ${state.pendingMessageCount}` : "";
3903
- const extra = [...statusEntries.entries()].map(([key, value]) => formatStatusEntry(key, value)).filter(Boolean).join(" · ");
3904
- const statusText = state?.isStreaming ? "Running" : "Idle";
3905
- const compactingText = state?.isCompacting ? " · Compacting" : "";
3906
- const queueText = state?.pendingMessageCount ? ` · Queue: ${state.pendingMessageCount}` : "";
3907
-
3908
- elements.sessionLine.textContent = `Status: ${statusText}${compactingText}${queueText}${extra ? ` · ${extra}` : ""} · Model: ${modelLabel(state?.model)} · Session: ${shortSessionLabel(state)}`;
3909
3965
 
3910
3966
  elements.stateDetails.replaceChildren();
3911
3967
  const details = {
@@ -4181,6 +4237,26 @@ function releaseNpmStreamHeader(label, lineCount, { live = false } = {}) {
4181
4237
  return header;
4182
4238
  }
4183
4239
 
4240
+ function renderReleaseNpmOutputDetails(key, streamHeader, terminal, controls = null) {
4241
+ const tabId = activeTabId || "default";
4242
+ const stateKey = `${tabId}:${key}`;
4243
+ const node = make("details", "release-npm-output-details");
4244
+ node.open = releaseNpmOutputExpandedByTab.get(stateKey) !== false;
4245
+ node.addEventListener("toggle", () => {
4246
+ releaseNpmOutputExpandedByTab.set(stateKey, node.open);
4247
+ if (node.open) requestAnimationFrame(() => { terminal.scrollTop = terminal.scrollHeight; });
4248
+ });
4249
+
4250
+ const summary = make("summary", "release-npm-output-summary");
4251
+ summary.title = "Expand or collapse command output in the Web UI";
4252
+ const toggle = make("span", "release-npm-output-toggle", "›");
4253
+ toggle.setAttribute("aria-hidden", "true");
4254
+ summary.append(toggle, streamHeader);
4255
+ node.append(summary, terminal);
4256
+ if (controls) node.append(controls);
4257
+ return node;
4258
+ }
4259
+
4184
4260
  function renderReleaseNpmOutputWidget() {
4185
4261
  if (!isOptionalFeatureEnabled("releaseNpm")) return null;
4186
4262
  const outputLines = getWidgetLines("release-npm:output");
@@ -4216,8 +4292,9 @@ function renderReleaseNpmOutputWidget() {
4216
4292
  }
4217
4293
 
4218
4294
  const controls = make("div", "release-npm-controls", details.controls || "Controls: /release-toggle expands/collapses · /release-abort stops subprocess");
4219
- node.append(header, streamHeader, terminal, controls);
4220
- requestAnimationFrame(() => { terminal.scrollTop = terminal.scrollHeight; });
4295
+ const outputDetails = renderReleaseNpmOutputDetails("release-npm:output", streamHeader, terminal, controls);
4296
+ node.append(header, outputDetails);
4297
+ requestAnimationFrame(() => { if (outputDetails.open) terminal.scrollTop = terminal.scrollHeight; });
4221
4298
  return node;
4222
4299
  }
4223
4300
 
@@ -4246,8 +4323,9 @@ function renderReleaseNpmLogWidget() {
4246
4323
  for (const line of logLines) {
4247
4324
  appendReleaseNpmTerminalLine(terminal, line);
4248
4325
  }
4249
- node.append(header, streamHeader, terminal);
4250
- requestAnimationFrame(() => { terminal.scrollTop = terminal.scrollHeight; });
4326
+ const outputDetails = renderReleaseNpmOutputDetails("release-npm:logs", streamHeader, terminal);
4327
+ node.append(header, outputDetails);
4328
+ requestAnimationFrame(() => { if (outputDetails.open) terminal.scrollTop = terminal.scrollHeight; });
4251
4329
  return node;
4252
4330
  }
4253
4331
 
@@ -4286,8 +4364,9 @@ function renderReleaseAurOutputWidget() {
4286
4364
  }
4287
4365
 
4288
4366
  const controls = make("div", "release-npm-controls", details.controls || "Controls: /release-aur toggle expands/collapses · /release-aur abort stops subprocess");
4289
- node.append(header, streamHeader, terminal, controls);
4290
- requestAnimationFrame(() => { terminal.scrollTop = terminal.scrollHeight; });
4367
+ const outputDetails = renderReleaseNpmOutputDetails("release-aur:output", streamHeader, terminal, controls);
4368
+ node.append(header, outputDetails);
4369
+ requestAnimationFrame(() => { if (outputDetails.open) terminal.scrollTop = terminal.scrollHeight; });
4291
4370
  return node;
4292
4371
  }
4293
4372
 
@@ -4316,8 +4395,9 @@ function renderReleaseAurLogWidget() {
4316
4395
  for (const line of logLines) {
4317
4396
  appendReleaseNpmTerminalLine(terminal, line);
4318
4397
  }
4319
- node.append(header, streamHeader, terminal);
4320
- requestAnimationFrame(() => { terminal.scrollTop = terminal.scrollHeight; });
4398
+ const outputDetails = renderReleaseNpmOutputDetails("release-aur:logs", streamHeader, terminal);
4399
+ node.append(header, outputDetails);
4400
+ requestAnimationFrame(() => { if (outputDetails.open) terminal.scrollTop = terminal.scrollHeight; });
4321
4401
  return node;
4322
4402
  }
4323
4403
 
@@ -8293,6 +8373,7 @@ async function refreshAll(tabContext = activeTabContext()) {
8293
8373
  refreshStats(tabContext),
8294
8374
  refreshWorkspace(tabContext),
8295
8375
  refreshNetworkStatus(),
8376
+ refreshWebuiVersion(),
8296
8377
  ]);
8297
8378
  if (!isCurrentTabContext(tabContext)) return;
8298
8379
  for (const result of results) {
@@ -8404,12 +8485,108 @@ async function closeNetworkAccess() {
8404
8485
  }
8405
8486
  }
8406
8487
 
8488
+ function setServerActionStatus(message = "", level = "info") {
8489
+ const status = elements.serverActionStatus;
8490
+ if (!status) return;
8491
+ status.textContent = message;
8492
+ status.hidden = !message;
8493
+ status.className = `server-action-status ${level} ${message ? "" : "muted"}`.trim();
8494
+ }
8495
+
8496
+ function updateServerActionButton() {
8497
+ const action = elements.serverActionSelect?.value || "";
8498
+ const button = elements.runServerActionButton;
8499
+ if (!button) return;
8500
+ button.disabled = !action;
8501
+ button.textContent = action === "restart" ? "Restart" : action === "stop" ? "Stop" : "Run";
8502
+ button.classList.toggle("danger", action === "stop");
8503
+ if (action) setServerActionStatus(action === "restart" ? "Ready to restart the Web UI server." : "Ready to stop the Web UI server.", "info");
8504
+ else setServerActionStatus();
8505
+ }
8506
+
8507
+ function setServerActionBusy(label) {
8508
+ if (elements.serverActionSelect) elements.serverActionSelect.disabled = true;
8509
+ if (elements.runServerActionButton) {
8510
+ elements.runServerActionButton.disabled = true;
8511
+ elements.runServerActionButton.textContent = label;
8512
+ }
8513
+ }
8514
+
8515
+ function resetServerActionControls() {
8516
+ if (elements.serverActionSelect) {
8517
+ elements.serverActionSelect.disabled = false;
8518
+ elements.serverActionSelect.value = "";
8519
+ }
8520
+ updateServerActionButton();
8521
+ }
8522
+
8523
+ async function waitForServerRestart() {
8524
+ for (let attempt = 0; attempt < 40; attempt++) {
8525
+ await delay(attempt === 0 ? 900 : 500);
8526
+ const message = `Restarting… reconnect attempt ${attempt + 1}/40`;
8527
+ setServerActionStatus(message, "warn");
8528
+ setServerRestartOverlay(true, message);
8529
+ try {
8530
+ await api("/api/health", { scoped: false });
8531
+ setBackendOffline(false);
8532
+ await initializeTabs();
8533
+ setServerRestartOverlay(false);
8534
+ setServerActionStatus("Server restarted and reconnected.", "success");
8535
+ addEvent("Pi Web UI server restarted", "warn");
8536
+ return true;
8537
+ } catch (error) {
8538
+ setBackendOffline(true, error);
8539
+ }
8540
+ }
8541
+ return false;
8542
+ }
8543
+
8544
+ async function restartServer() {
8545
+ if (!confirm("Restart the Pi Web UI server?\n\nThis briefly disconnects browser clients and restarts the Pi tabs managed by this Web UI.")) return;
8546
+
8547
+ setServerActionBusy("Restarting…");
8548
+ setServerActionStatus("Restart requested. Waiting for the server to come back…", "warn");
8549
+ setServerRestartOverlay(true, "Restart requested. Waiting for the server to come back…");
8550
+ try {
8551
+ await api("/api/restart", { method: "POST", scoped: false });
8552
+ addEvent("Pi Web UI server restart requested", "warn");
8553
+ } catch (error) {
8554
+ if (!error?.backendOffline) {
8555
+ const missingRestartEndpoint = error.statusCode === 404 || /not found/i.test(error.message || "");
8556
+ const message = missingRestartEndpoint
8557
+ ? "Restart is not available in the currently running server. Stop/start manually once to load the new backend."
8558
+ : error.message || String(error);
8559
+ addEvent(message, "error");
8560
+ setServerRestartOverlay(false);
8561
+ resetServerActionControls();
8562
+ setServerActionStatus(message, "error");
8563
+ return;
8564
+ }
8565
+ addEvent("Pi Web UI server connection dropped during restart request", "warn");
8566
+ }
8567
+
8568
+ setBackendOffline(true, new Error("restart requested from side panel"));
8569
+ const restarted = await waitForServerRestart();
8570
+ if (elements.serverActionSelect) {
8571
+ elements.serverActionSelect.disabled = false;
8572
+ elements.serverActionSelect.value = "";
8573
+ }
8574
+ updateServerActionButton();
8575
+ if (restarted) {
8576
+ setServerActionStatus("Server restarted and reconnected.", "success");
8577
+ } else {
8578
+ setServerRestartOverlay(false);
8579
+ setBackendOffline(true, new Error("restart reconnect timed out"));
8580
+ setServerActionStatus("Restart requested, but the server did not reconnect automatically.", "error");
8581
+ addEvent("Pi Web UI server did not come back online after restart request", "error");
8582
+ }
8583
+ }
8584
+
8407
8585
  async function stopServer() {
8408
8586
  if (!confirm("Stop the Pi Web UI server?\n\nThis disconnects all browser clients and stops the Pi tabs managed by this Web UI.")) return;
8409
8587
 
8410
- const button = elements.stopServerButton;
8411
- button.disabled = true;
8412
- button.textContent = "Stopping…";
8588
+ setServerActionBusy("Stopping…");
8589
+ setServerActionStatus("Stop requested. The Web UI will disconnect.", "warn");
8413
8590
  try {
8414
8591
  await api("/api/shutdown", { method: "POST", scoped: false });
8415
8592
  addEvent("Pi Web UI server stop requested", "warn");
@@ -8421,11 +8598,17 @@ async function stopServer() {
8421
8598
  return;
8422
8599
  }
8423
8600
  addEvent(error.message || String(error), "error");
8424
- button.disabled = false;
8425
- button.textContent = "Stop Server";
8601
+ resetServerActionControls();
8602
+ setServerActionStatus(error.message || String(error), "error");
8426
8603
  }
8427
8604
  }
8428
8605
 
8606
+ async function runSelectedServerAction() {
8607
+ const action = elements.serverActionSelect?.value || "";
8608
+ if (action === "restart") await restartServer();
8609
+ else if (action === "stop") await stopServer();
8610
+ }
8611
+
8429
8612
  function appShortcutModelLabel(model) {
8430
8613
  return model ? `${model.provider}/${model.id}` : "unknown model";
8431
8614
  }
@@ -8840,8 +9023,11 @@ function showNextDialog() {
8840
9023
  button.type = "button";
8841
9024
  if (isGuardrailDialog && /^Block$/i.test(optionLabel)) button.classList.add("guardrail-safe-action");
8842
9025
  if (isGuardrailDialog && /^Allow/i.test(optionLabel)) button.classList.add("guardrail-allow-action");
8843
- if (isReleaseDialog && /^Yes$/i.test(optionLabel)) button.classList.add("primary", "release-publish-action");
8844
- if (isReleaseDialog && /^No$/i.test(optionLabel)) button.classList.add("release-cancel-action");
9026
+ if (isReleaseDialog && /^(?:Yes|All eligible packages\b|Publish selected packages \([1-9]\d*\))/.test(optionLabel)) button.classList.add("primary", "release-publish-action");
9027
+ if (isReleaseDialog && /^Publish selected packages \(select at least one\)$/i.test(optionLabel)) button.classList.add("release-publish-disabled-action");
9028
+ if (isReleaseDialog && /^\[x\]/.test(optionLabel)) button.classList.add("release-target-option", "release-target-selected");
9029
+ if (isReleaseDialog && /^\[ \]/.test(optionLabel)) button.classList.add("release-target-option");
9030
+ if (isReleaseDialog && /^(?:No|Cancel)$/i.test(optionLabel)) button.classList.add("release-cancel-action");
8845
9031
  button.addEventListener("click", () => sendDialogResponse({ type: "extension_ui_response", id: request.id, value: optionLabel, tabId: request.tabId }));
8846
9032
  options.append(button);
8847
9033
  }
@@ -8889,6 +9075,8 @@ function handleEvent(event) {
8889
9075
  const tabContext = activeTabContext(event.tabId || activeTabId);
8890
9076
  switch (event.type) {
8891
9077
  case "webui_connected":
9078
+ setWebuiVersion(event.version);
9079
+ setWebuiDevServer(isWebuiDevMetadata(event));
8892
9080
  addEvent(`connected to ${event.tabTitle || "terminal"} for ${event.cwd}`);
8893
9081
  scheduleForegroundReconcile("event stream reconnect", 0);
8894
9082
  break;
@@ -9299,7 +9487,9 @@ if (elements.backgroundClearButton) {
9299
9487
  elements.backgroundClearButton.addEventListener("click", () => clearCustomBackground().catch((error) => addEvent(error.message || String(error), "error")));
9300
9488
  }
9301
9489
  elements.openNetworkButton.addEventListener("click", openToNetwork);
9302
- elements.stopServerButton.addEventListener("click", stopServer);
9490
+ elements.serverActionSelect.addEventListener("change", updateServerActionButton);
9491
+ elements.runServerActionButton.addEventListener("click", () => runSelectedServerAction().catch((error) => addEvent(error.message || String(error), "error")));
9492
+ updateServerActionButton();
9303
9493
  elements.agentDoneNotificationsToggle.addEventListener("change", () => {
9304
9494
  setAgentDoneNotificationsEnabled(elements.agentDoneNotificationsToggle.checked, {
9305
9495
  requestPermission: elements.agentDoneNotificationsToggle.checked,
package/public/index.html CHANGED
@@ -36,6 +36,15 @@
36
36
  </div>
37
37
  </section>
38
38
 
39
+ <section id="serverRestartPanel" class="server-restart-panel" aria-live="assertive" hidden>
40
+ <div class="server-restart-card" role="status">
41
+ <div class="server-restart-spinner" aria-hidden="true"></div>
42
+ <span class="server-restart-kicker">Restarting</span>
43
+ <h1>Restarting Pi Web UI server</h1>
44
+ <p id="serverRestartMessage">Waiting for the server to come back…</p>
45
+ </div>
46
+ </section>
47
+
39
48
  <main class="layout">
40
49
  <section class="chat-panel">
41
50
  <header class="terminal-tabs-shell">
@@ -151,8 +160,11 @@
151
160
  <div class="side-panel-header">
152
161
  <div>
153
162
  <span class="side-panel-kicker">Pi Web UI</span>
154
- <strong>Control Deck</strong>
155
- <p id="sessionLine" class="side-panel-session-line muted">Connecting…</p>
163
+ <strong class="side-panel-title">
164
+ <span>Control Deck</span>
165
+ <span id="webuiVersionBadge" class="webui-version-badge" aria-label="Pi Web UI version" hidden></span>
166
+ <span id="webuiDevBadge" class="webui-dev-badge" aria-label="Pi Web UI dev server" hidden>DEV</span>
167
+ </strong>
156
168
  </div>
157
169
  <button id="toggleSidePanelButton" class="side-panel-toggle-button" type="button" aria-controls="sidePanel" aria-expanded="true" aria-label="Collapse side panel" title="Collapse side panel">
158
170
  <span class="side-panel-button-icon" aria-hidden="true">
@@ -217,8 +229,16 @@
217
229
  <button id="openNetworkButton" type="button">Open to network</button>
218
230
  </div>
219
231
  <div class="control-field server-control-field">
220
- <label>Server</label>
221
- <button id="stopServerButton" class="danger" type="button" title="Stop the Pi Web UI server and disconnect all browser clients" aria-label="Stop the Pi Web UI server">Stop Server</button>
232
+ <label for="serverActionSelect">Server</label>
233
+ <div class="server-action-row">
234
+ <select id="serverActionSelect" title="Server action" aria-label="Server action">
235
+ <option value="" selected>Choose action…</option>
236
+ <option value="restart">Restart Server</option>
237
+ <option value="stop">Stop Server</option>
238
+ </select>
239
+ <button id="runServerActionButton" type="button" disabled>Run</button>
240
+ </div>
241
+ <div id="serverActionStatus" class="server-action-status muted" role="status" aria-live="polite"></div>
222
242
  </div>
223
243
  <div class="control-field notification-control-field">
224
244
  <span class="control-label">Notifications</span>
@@ -1,4 +1,4 @@
1
- const CACHE_NAME = "pi-webui-pwa-v16";
1
+ const CACHE_NAME = "pi-webui-pwa-v18";
2
2
  const APP_SHELL = [
3
3
  "/",
4
4
  "/index.html",