diff --git a/.changeset/restart-via-systemd-run.md b/.changeset/restart-via-systemd-run.md
new file mode 100644
index 0000000..310def2
--- /dev/null
+++ b/.changeset/restart-via-systemd-run.md
@@ -0,0 +1,5 @@
+---
+"@jmfederico/pi-web": patch
+---
+
+Run the suggested Linux restart commands inside a detached transient systemd user service (`systemd-run --user`) instead of directly. The restart now completes even when the launching PI WEB terminal is killed by restarting the session daemon, and its output can be inspected with `journalctl --user -u pi-web-restart`.
diff --git a/.changeset/updates-panel-run-commands.md b/.changeset/updates-panel-run-commands.md
new file mode 100644
index 0000000..21ad939
--- /dev/null
+++ b/.changeset/updates-panel-run-commands.md
@@ -0,0 +1,5 @@
+---
+"@jmfederico/pi-web": patch
+---
+
+Make the Updates panel actionable: every suggested command now has both a Copy and a Run button (Run executes it in a workspace terminal), a single recommended all-in-one command is shown at the top so users do not have to choose, and the remaining commands are grouped as clearly optional additional commands.
diff --git a/pi-web-plugins/updates/pi-web-plugin.ts b/pi-web-plugins/updates/pi-web-plugin.ts
index 4a11efc..95b8792 100644
--- a/pi-web-plugins/updates/pi-web-plugin.ts
+++ b/pi-web-plugins/updates/pi-web-plugin.ts
@@ -1,5 +1,49 @@
import type { TemplateResult } from "lit";
-import type { HtmlTemplateTag, PiWebComponentStatus, PiWebInstallationInfo, PiWebPlugin, PiWebStatusMessage, PiWebStatusResponse, PluginRuntimeState } from "@jmfederico/pi-web/plugin-api";
+import type { HtmlTemplateTag, PiWebComponentStatus, PiWebInstallationInfo, PiWebPlugin, PiWebStatusMessage, PiWebStatusResponse, PluginRuntimeState, WorkspacePanelTerminal } from "@jmfederico/pi-web/plugin-api";
+
+interface CommandEntry {
+ label: string;
+ command: string;
+}
+
+function runCommandInTerminal(terminal: WorkspacePanelTerminal, label: string, command: string): void {
+ void terminal.runCommand({
+ title: label,
+ command,
+ open: true,
+ metadata: { "pi.plugin": "updates" },
+ }).catch((error: unknown) => {
+ console.error(`Updates plugin failed to run "${label}"`, error);
+ });
+}
+
+// The single command users should run when they do not want to think: if an
+// update is available, `commands.update` already chains the update and a full
+// restart; otherwise, when anything is stale, a full restart is enough.
+function recommendedCommand(status: PiWebStatusResponse): CommandEntry | undefined {
+ const { commands, release, components } = status;
+ if (release.updateAvailable && typeof commands.update === "string" && commands.update !== "") {
+ return { label: "Update & restart everything", command: commands.update };
+ }
+ const restartNeeded = components.web.stale || components.sessiond.stale || !components.sessiond.available;
+ if (restartNeeded && typeof commands.restart === "string" && commands.restart !== "") {
+ return { label: "Restart everything", command: commands.restart };
+ }
+ return undefined;
+}
+
+function additionalCommands(status: PiWebStatusResponse, recommended: CommandEntry | undefined): CommandEntry[] {
+ return [
+ ["Update", status.commands.update],
+ ["Restart all", status.commands.restart],
+ ["Restart Web/UI", status.commands.restartWeb],
+ ["Restart session daemon", status.commands.restartSessiond],
+ ["Status", status.commands.status],
+ ]
+ .filter((entry): entry is [string, string] => typeof entry[1] === "string" && entry[1] !== "")
+ .filter(([, command]) => command !== recommended?.command)
+ .map(([label, command]) => ({ label, command }));
+}
function messagesFor(state: PluginRuntimeState | undefined): PiWebStatusMessage[] {
return state?.piWebStatus?.messages ?? [];
@@ -57,35 +101,48 @@ function renderComponent(html: HtmlTemplateTag, component: PiWebComponentStatus)
`;
}
-function renderCommand(html: HtmlTemplateTag, label: string, command: string): TemplateResult {
+function renderCommandActions(html: HtmlTemplateTag, terminal: WorkspacePanelTerminal | undefined, label: string, command: string): TemplateResult {
+ return html`
+
+
+ ${terminal === undefined ? null : html``}
+
+ `;
+}
+
+function renderCommand(html: HtmlTemplateTag, terminal: WorkspacePanelTerminal | undefined, label: string, command: string): TemplateResult {
return html`
${label}
${command}
-
+ ${renderCommandActions(html, terminal, label, command)}
`;
}
-function renderCommands(html: HtmlTemplateTag, status: PiWebStatusResponse): TemplateResult | undefined {
- const commands = [
- ["Update", status.commands.update],
- ["Restart all", status.commands.restart],
- ["Restart Web/UI", status.commands.restartWeb],
- ["Restart session daemon", status.commands.restartSessiond],
- ["Status", status.commands.status],
- ].filter((entry): entry is [string, string] => typeof entry[1] === "string" && entry[1] !== "");
-
- if (commands.length === 0) return undefined;
+function renderCommands(html: HtmlTemplateTag, terminal: WorkspacePanelTerminal | undefined, status: PiWebStatusResponse): TemplateResult | undefined {
+ const recommended = recommendedCommand(status);
+ const additional = additionalCommands(status, recommended);
+ if (recommended === undefined && additional.length === 0) return undefined;
return html`
-
- Suggested commands
- ${commands.map(([label, command]) => renderCommand(html, label, command))}
-
+ ${recommended === undefined ? null : html`
+
+ Recommended
+ Run this one command to bring this installation fully up to date. Nothing else is required.
+ ${renderCommand(html, terminal, recommended.label, recommended.command)}
+
+ `}
+ ${additional.length === 0 ? null : html`
+
+ ${recommended === undefined ? "Suggested commands" : "Additional commands (optional)"}
+ ${recommended === undefined ? null : html`Only needed for finer control, such as restarting a single service.
`}
+ ${additional.map((entry) => renderCommand(html, terminal, entry.label, entry.command))}
+
+ `}
`;
}
-function renderUpdatesPanel(html: HtmlTemplateTag, state: PluginRuntimeState | undefined): TemplateResult {
+function renderUpdatesPanel(html: HtmlTemplateTag, terminal: WorkspacePanelTerminal | undefined, state: PluginRuntimeState | undefined): TemplateResult {
const status = statusFor(state);
if (status === undefined) {
return html`
@@ -108,6 +165,11 @@ function renderUpdatesPanel(html: HtmlTemplateTag, state: PluginRuntimeState | u
.updates-version-row small { grid-column: 1 / -1; color: var(--pi-muted); }
.updates-command { min-width: 0; display: grid; grid-template-columns: minmax(90px, auto) minmax(0, 1fr) auto; gap: 8px; align-items: center; }
.updates-command code { overflow: auto; border: 1px solid var(--pi-border-muted); border-radius: 6px; background: var(--pi-bg); padding: 5px 7px; white-space: nowrap; }
+ .updates-command-inline { grid-template-columns: minmax(0, 1fr) auto; }
+ .updates-command-actions { display: inline-flex; gap: 6px; }
+ .updates-command-actions button.primary { border-color: var(--pi-accent-border); color: var(--pi-text-bright); }
+ .updates-recommended { border: 1px solid var(--pi-accent-border); border-radius: 8px; padding: 10px; background: var(--pi-surface); }
+ .updates-recommended > strong { color: var(--pi-text-bright); }
.updates-meta { display: grid; gap: 2px; color: var(--pi-muted); font-size: 12px; }
@media (max-width: 520px) {
.updates-command { grid-template-columns: minmax(0, 1fr) auto; }
@@ -121,7 +183,12 @@ function renderUpdatesPanel(html: HtmlTemplateTag, state: PluginRuntimeState | u
${message.title}${message.severity}
${message.body}
- ${message.command === undefined ? null : html`${message.command}`}
+ ${message.command === undefined ? null : html`
+
+ ${message.command}
+ ${renderCommandActions(html, terminal, message.title, message.command)}
+
+ `}
`)}
@@ -132,7 +199,7 @@ function renderUpdatesPanel(html: HtmlTemplateTag, state: PluginRuntimeState | u
${renderComponent(html, status.components.sessiond)}
- ${renderCommands(html, status)}
+ ${renderCommands(html, terminal, status)}