Hide hosted dashboard update controls

This commit is contained in:
Shannon Sands 2026-06-16 11:24:14 +10:00 committed by Teknium
parent 55cb4103be
commit 0b6b29a30c
7 changed files with 242 additions and 82 deletions

View File

@ -1224,6 +1224,17 @@ def _default_hermes_root_is_opt_data() -> bool:
return root == _HOSTED_MANAGED_FILES_ROOT return root == _HOSTED_MANAGED_FILES_ROOT
def _dashboard_hosted_agent_mode() -> bool:
"""Return true for the hosted/container dashboard layout.
Hosted agent dashboards run with the Hermes root at ``/opt/data``. This is
the same signal the Files page uses to lock browsing to the managed data
directory, and it keeps local remote-auth dashboards from being mistaken for
hosted service instances.
"""
return _default_hermes_root_is_opt_data()
def _managed_files_policy(request: Request, *, create_root: bool = True) -> ManagedFilesPolicy: def _managed_files_policy(request: Request, *, create_root: bool = True) -> ManagedFilesPolicy:
raw_forced_root = os.environ.get(_MANAGED_FILES_ROOT_ENV, "").strip() raw_forced_root = os.environ.get(_MANAGED_FILES_ROOT_ENV, "").strip()
if raw_forced_root: if raw_forced_root:
@ -1654,6 +1665,7 @@ async def get_status():
"release_date": __release_date__, "release_date": __release_date__,
"config_version": current_ver, "config_version": current_ver,
"latest_config_version": latest_ver, "latest_config_version": latest_ver,
"can_update_hermes": not _dashboard_hosted_agent_mode(),
"gateway_running": gateway_running, "gateway_running": gateway_running,
"gateway_state": gateway_state, "gateway_state": gateway_state,
"gateway_platforms": gateway_platforms, "gateway_platforms": gateway_platforms,
@ -2165,6 +2177,21 @@ async def restart_gateway():
@app.post("/api/hermes/update") @app.post("/api/hermes/update")
async def update_hermes(): async def update_hermes():
"""Kick off ``hermes update`` in the background.""" """Kick off ``hermes update`` in the background."""
if _dashboard_hosted_agent_mode():
message = (
"Hermes updates are managed by the hosted agent service for this "
"dashboard. The built-in local updater is disabled here."
)
_record_completed_action("hermes-update", message, exit_code=1)
return {
"ok": False,
"pid": None,
"name": "hermes-update",
"error": "hosted_update_managed",
"message": message,
"update_command": "managed by hosted agent service",
}
install_method = detect_install_method(PROJECT_ROOT) install_method = detect_install_method(PROJECT_ROOT)
if install_method == "docker": if install_method == "docker":
message = format_docker_update_message() message = format_docker_update_message()
@ -2264,6 +2291,17 @@ async def check_hermes_update(force: bool = False):
desktop's remote update overlay renders this as "what's desktop's remote update overlay renders this as "what's
changed". Additive: existing consumers ignore it. changed". Additive: existing consumers ignore it.
""" """
if _dashboard_hosted_agent_mode():
return {
"install_method": "hosted",
"current_version": __version__,
"behind": None,
"update_available": False,
"can_apply": False,
"update_command": "managed by hosted agent service",
"message": "Hermes updates are managed by the hosted agent service.",
}
install_method = detect_install_method(PROJECT_ROOT) install_method = detect_install_method(PROJECT_ROOT)
update_command = recommended_update_command_for_method(install_method) update_command = recommended_update_command_for_method(install_method)

View File

@ -772,6 +772,25 @@ class TestUpdateCheckEndpoint:
assert body["message"] assert body["message"]
assert body["behind"] is None assert body["behind"] is None
def test_hosted_dashboard_is_not_applyable(self, monkeypatch):
import hermes_cli.web_server as ws
monkeypatch.setattr(ws, "_dashboard_hosted_agent_mode", lambda: True)
monkeypatch.setattr(
ws,
"detect_install_method",
lambda *a, **k: pytest.fail(
"hosted update check should not probe install method"
),
)
body = self.client.get("/api/hermes/update/check").json()
assert body["install_method"] == "hosted"
assert body["can_apply"] is False
assert body["update_available"] is False
assert body["behind"] is None
assert "hosted agent service" in body["message"]
def test_check_failure_is_soft(self, monkeypatch): def test_check_failure_is_soft(self, monkeypatch):
import hermes_cli.web_server as ws import hermes_cli.web_server as ws
import hermes_cli.banner as banner import hermes_cli.banner as banner

View File

@ -245,6 +245,16 @@ class TestWebServerEndpoints:
assert "version" in data assert "version" in data
assert "hermes_home" in data assert "hermes_home" in data
assert "active_sessions" in data assert "active_sessions" in data
assert data["can_update_hermes"] is True
def test_get_status_hides_update_capability_in_hosted_mode(self, monkeypatch):
import hermes_cli.web_server as web_server
monkeypatch.setattr(web_server, "_dashboard_hosted_agent_mode", lambda: True)
resp = self.client.get("/api/status")
assert resp.status_code == 200
assert resp.json()["can_update_hermes"] is False
# ── GET /api/media (remote image display) ─────────────────────────── # ── GET /api/media (remote image display) ───────────────────────────
@ -912,6 +922,48 @@ class TestWebServerEndpoints:
assert status_data["pid"] is None assert status_data["pid"] is None
assert any("docker pull nousresearch/hermes-agent:latest" in line for line in status_data["lines"]) assert any("docker pull nousresearch/hermes-agent:latest" in line for line in status_data["lines"])
def test_update_hermes_returns_hosted_guidance_without_spawning(self, monkeypatch):
import hermes_cli.web_server as web_server
spawned = False
detected = False
def fail_spawn(*_args, **_kwargs):
nonlocal spawned
spawned = True
raise AssertionError("hosted update guard should not spawn hermes update")
def fail_detect(*_args, **_kwargs):
nonlocal detected
detected = True
raise AssertionError("hosted update guard should not detect install method")
monkeypatch.setattr(web_server, "_dashboard_hosted_agent_mode", lambda: True)
monkeypatch.setattr(web_server, "detect_install_method", fail_detect)
monkeypatch.setattr(web_server, "_spawn_hermes_action", fail_spawn)
web_server._ACTION_PROCS.pop("hermes-update", None)
web_server._ACTION_RESULTS.pop("hermes-update", None)
resp = self.client.post("/api/hermes/update")
assert resp.status_code == 200
data = resp.json()
assert data["ok"] is False
assert data["name"] == "hermes-update"
assert data["pid"] is None
assert data["error"] == "hosted_update_managed"
assert "hosted agent service" in data["message"]
assert spawned is False
assert detected is False
status = self.client.get("/api/actions/hermes-update/status")
assert status.status_code == 200
status_data = status.json()
assert status_data["running"] is False
assert status_data["exit_code"] == 1
assert status_data["pid"] is None
assert any("hosted agent service" in line for line in status_data["lines"])
def test_update_hermes_spawns_on_non_docker_install(self, monkeypatch): def test_update_hermes_spawns_on_non_docker_install(self, monkeypatch):
import hermes_cli.web_server as web_server import hermes_cli.web_server as web_server

View File

@ -5,6 +5,8 @@ import {
useRef, useRef,
useState, useState,
type ComponentType, type ComponentType,
type FocusEvent,
type MouseEvent,
type ReactNode, type ReactNode,
} from "react"; } from "react";
import { createPortal } from "react-dom"; import { createPortal } from "react-dom";
@ -812,26 +814,33 @@ function SidebarNavLink({
t, t,
}: SidebarNavLinkProps) { }: SidebarNavLinkProps) {
const { path, label, labelKey, icon: Icon } = item; const { path, label, labelKey, icon: Icon } = item;
const liRef = useRef<HTMLLIElement>(null);
const [hovered, setHovered] = useState(false); const [hovered, setHovered] = useState(false);
const [tooltipAnchor, setTooltipAnchor] = useState<HTMLElement | null>(null);
const navLabel = labelKey const navLabel = labelKey
? ((t.app.nav as Record<string, string>)[labelKey] ?? label) ? ((t.app.nav as Record<string, string>)[labelKey] ?? label)
: label; : label;
const showTooltip = (event: MouseEvent<HTMLElement> | FocusEvent<HTMLElement>) => {
setHovered(true);
setTooltipAnchor(event.currentTarget);
};
const hideTooltip = () => {
setHovered(false);
setTooltipAnchor(null);
};
return ( return (
<li <li
ref={liRef} onMouseEnter={collapsed ? showTooltip : undefined}
onMouseEnter={collapsed ? () => setHovered(true) : undefined} onMouseLeave={collapsed ? hideTooltip : undefined}
onMouseLeave={collapsed ? () => setHovered(false) : undefined}
> >
<NavLink <NavLink
to={path} to={path}
end={path === "/sessions"} end={path === "/sessions"}
onClick={closeMobile} onClick={closeMobile}
aria-label={collapsed ? navLabel : undefined} aria-label={collapsed ? navLabel : undefined}
onFocus={collapsed ? () => setHovered(true) : undefined} onFocus={collapsed ? showTooltip : undefined}
onBlur={collapsed ? () => setHovered(false) : undefined} onBlur={collapsed ? hideTooltip : undefined}
className={({ isActive }) => className={({ isActive }) =>
cn( cn(
"group/nav relative flex items-center gap-3", "group/nav relative flex items-center gap-3",
@ -877,8 +886,8 @@ function SidebarNavLink({
)} )}
</NavLink> </NavLink>
{collapsed && hovered && liRef.current && ( {collapsed && hovered && tooltipAnchor && (
<SidebarTooltip anchor={liRef.current} label={navLabel} warmRef={tooltipWarmRef} /> <SidebarTooltip anchor={tooltipAnchor} label={navLabel} warmRef={tooltipWarmRef} />
)} )}
</li> </li>
); );
@ -894,6 +903,7 @@ function SidebarSystemActions({
const navigate = useNavigate(); const navigate = useNavigate();
const { activeAction, isBusy, isRunning, pendingAction, runAction } = const { activeAction, isBusy, isRunning, pendingAction, runAction } =
useSystemActions(); useSystemActions();
const canUpdateHermes = status?.can_update_hermes === true;
const items: SystemActionItem[] = [ const items: SystemActionItem[] = [
{ {
@ -903,14 +913,16 @@ function SidebarSystemActions({
runningLabel: t.status.restartingGateway, runningLabel: t.status.restartingGateway,
spin: true, spin: true,
}, },
{ ];
if (canUpdateHermes) {
items.push({
action: "update", action: "update",
icon: Download, icon: Download,
label: t.status.updateHermes, label: t.status.updateHermes,
runningLabel: t.status.updatingHermes, runningLabel: t.status.updatingHermes,
spin: false, spin: false,
}, });
]; }
const handleClick = (action: SystemAction) => { const handleClick = (action: SystemAction) => {
if (isBusy) return; if (isBusy) return;
@ -971,24 +983,31 @@ function SystemActionButton({
tooltipWarmRef, tooltipWarmRef,
}: SystemActionButtonProps) { }: SystemActionButtonProps) {
const { icon: Icon, label, runningLabel, spin } = item; const { icon: Icon, label, runningLabel, spin } = item;
const liRef = useRef<HTMLLIElement>(null);
const [hovered, setHovered] = useState(false); const [hovered, setHovered] = useState(false);
const [tooltipAnchor, setTooltipAnchor] = useState<HTMLElement | null>(null);
const busy = isPending || isActionRunning; const busy = isPending || isActionRunning;
const displayLabel = isActionRunning ? runningLabel : label; const displayLabel = isActionRunning ? runningLabel : label;
const showTooltip = (event: MouseEvent<HTMLElement> | FocusEvent<HTMLElement>) => {
setHovered(true);
setTooltipAnchor(event.currentTarget);
};
const hideTooltip = () => {
setHovered(false);
setTooltipAnchor(null);
};
return ( return (
<li <li
ref={liRef} onMouseEnter={collapsed ? showTooltip : undefined}
onMouseEnter={collapsed ? () => setHovered(true) : undefined} onMouseLeave={collapsed ? hideTooltip : undefined}
onMouseLeave={collapsed ? () => setHovered(false) : undefined}
> >
<button <button
onClick={onClick} onClick={onClick}
disabled={disabled} disabled={disabled}
aria-busy={busy} aria-busy={busy}
aria-label={collapsed ? displayLabel : undefined} aria-label={collapsed ? displayLabel : undefined}
onFocus={collapsed ? () => setHovered(true) : undefined} onFocus={collapsed ? showTooltip : undefined}
onBlur={collapsed ? () => setHovered(false) : undefined} onBlur={collapsed ? hideTooltip : undefined}
type="button" type="button"
className={cn( className={cn(
"group/action relative flex w-full items-center gap-3", "group/action relative flex w-full items-center gap-3",
@ -1036,8 +1055,8 @@ function SystemActionButton({
)} )}
</button> </button>
{collapsed && hovered && liRef.current && ( {collapsed && hovered && tooltipAnchor && (
<SidebarTooltip anchor={liRef.current} label={displayLabel} warmRef={tooltipWarmRef} /> <SidebarTooltip anchor={tooltipAnchor} label={displayLabel} warmRef={tooltipWarmRef} />
)} )}
</li> </li>
); );
@ -1049,18 +1068,25 @@ function SidebarIconWithTooltip({
label, label,
tooltipWarmRef, tooltipWarmRef,
}: SidebarIconWithTooltipProps) { }: SidebarIconWithTooltipProps) {
const ref = useRef<HTMLDivElement>(null);
const [hovered, setHovered] = useState(false); const [hovered, setHovered] = useState(false);
const [tooltipAnchor, setTooltipAnchor] = useState<HTMLElement | null>(null);
const showTooltip = (event: MouseEvent<HTMLDivElement>) => {
setHovered(true);
setTooltipAnchor(event.currentTarget);
};
const hideTooltip = () => {
setHovered(false);
setTooltipAnchor(null);
};
return ( return (
<div <div
ref={ref}
className={cn( className={cn(
"relative w-fit", "relative w-fit",
collapsed && "group/icon", collapsed && "group/icon",
)} )}
onMouseEnter={collapsed ? () => setHovered(true) : undefined} onMouseEnter={collapsed ? showTooltip : undefined}
onMouseLeave={collapsed ? () => setHovered(false) : undefined} onMouseLeave={collapsed ? hideTooltip : undefined}
> >
{children} {children}
@ -1071,8 +1097,8 @@ function SidebarIconWithTooltip({
/> />
)} )}
{collapsed && hovered && ref.current && ( {collapsed && hovered && tooltipAnchor && (
<SidebarTooltip anchor={ref.current} label={label} warmRef={tooltipWarmRef} /> <SidebarTooltip anchor={tooltipAnchor} label={label} warmRef={tooltipWarmRef} />
)} )}
</div> </div>
); );
@ -1080,8 +1106,8 @@ function SidebarIconWithTooltip({
function GatewayDot({ collapsed, status, tooltipWarmRef }: GatewayDotProps) { function GatewayDot({ collapsed, status, tooltipWarmRef }: GatewayDotProps) {
const { t } = useI18n(); const { t } = useI18n();
const ref = useRef<HTMLDivElement>(null);
const [hovered, setHovered] = useState(false); const [hovered, setHovered] = useState(false);
const [tooltipAnchor, setTooltipAnchor] = useState<HTMLElement | null>(null);
const toneToColor: Record<string, string> = { const toneToColor: Record<string, string> = {
"text-success": "bg-success", "text-success": "bg-success",
@ -1101,10 +1127,17 @@ function GatewayDot({ collapsed, status, tooltipWarmRef }: GatewayDotProps) {
color = toneToColor[gw.tone] ?? "bg-muted-foreground"; color = toneToColor[gw.tone] ?? "bg-muted-foreground";
label = `${t.status.gateway} ${gw.label}`; label = `${t.status.gateway} ${gw.label}`;
} }
const showTooltip = (event: MouseEvent<HTMLDivElement> | FocusEvent<HTMLDivElement>) => {
setHovered(true);
setTooltipAnchor(event.currentTarget);
};
const hideTooltip = () => {
setHovered(false);
setTooltipAnchor(null);
};
return ( return (
<div <div
ref={ref}
className={cn( className={cn(
"hidden lg:flex py-3 pl-[1.625rem] transition-opacity duration-300", "hidden lg:flex py-3 pl-[1.625rem] transition-opacity duration-300",
collapsed ? "lg:opacity-100" : "lg:opacity-0 lg:h-0 lg:py-0 lg:overflow-hidden", collapsed ? "lg:opacity-100" : "lg:opacity-0 lg:h-0 lg:py-0 lg:overflow-hidden",
@ -1112,18 +1145,18 @@ function GatewayDot({ collapsed, status, tooltipWarmRef }: GatewayDotProps) {
role="status" role="status"
aria-label={label} aria-label={label}
tabIndex={collapsed ? 0 : -1} tabIndex={collapsed ? 0 : -1}
onMouseEnter={collapsed ? () => setHovered(true) : undefined} onMouseEnter={collapsed ? showTooltip : undefined}
onMouseLeave={collapsed ? () => setHovered(false) : undefined} onMouseLeave={collapsed ? hideTooltip : undefined}
onFocus={collapsed ? () => setHovered(true) : undefined} onFocus={collapsed ? showTooltip : undefined}
onBlur={collapsed ? () => setHovered(false) : undefined} onBlur={collapsed ? hideTooltip : undefined}
> >
<span <span
aria-hidden aria-hidden
className={cn("h-1.5 w-1.5 rounded-full", color)} className={cn("h-1.5 w-1.5 rounded-full", color)}
/> />
{hovered && ref.current && ( {hovered && tooltipAnchor && (
<SidebarTooltip anchor={ref.current} label={label} warmRef={tooltipWarmRef} /> <SidebarTooltip anchor={tooltipAnchor} label={label} warmRef={tooltipWarmRef} />
)} )}
</div> </div>
); );
@ -1133,11 +1166,16 @@ function SidebarTooltip({ anchor, label, warmRef }: SidebarTooltipProps) {
const rect = anchor.getBoundingClientRect(); const rect = anchor.getBoundingClientRect();
const sidebar = document.getElementById("app-sidebar"); const sidebar = document.getElementById("app-sidebar");
const sidebarRight = sidebar?.getBoundingClientRect().right ?? rect.right; const sidebarRight = sidebar?.getBoundingClientRect().right ?? rect.right;
const [isWarm, setIsWarm] = useState(false);
const isWarm = warmRef ? Date.now() - warmRef.current < 300 : false;
useEffect(() => { useEffect(() => {
if (warmRef) warmRef.current = Date.now(); if (!warmRef) {
setIsWarm(false);
return;
}
const now = Date.now();
setIsWarm(now - warmRef.current < 300);
warmRef.current = now;
return () => { return () => {
if (warmRef) warmRef.current = Date.now(); if (warmRef) warmRef.current = Date.now();
}; };

View File

@ -74,19 +74,17 @@ export function SystemActionsProvider({
setActiveAction(action); setActiveAction(action);
} else { } else {
const resp = await api.updateHermes(); const resp = await api.updateHermes();
// In a Docker install the image is immutable, so `hermes update` // Some installs cannot apply updates from inside the dashboard. The
// can't apply — the endpoint returns 200 with a structured // endpoint returns a structured {ok:false, message, update_command}
// {ok:false, error:"docker_update_unsupported", message, update_command} // envelope instead of spawning the action; surface that guidance
// envelope instead of spawning the action (see #34347 / #36263). // rather than polling a synthetic failed action.
// Surface that guidance to the user rather than starting the poll, if (!resp.ok) {
// which would otherwise report a generic "failed (exit 1)".
if (!resp.ok && resp.error === "docker_update_unsupported") {
const cmd = resp.update_command ? ` ${resp.update_command}` : ""; const cmd = resp.update_command ? ` ${resp.update_command}` : "";
setToast({ setToast({
type: "success", type: "success",
message: message:
(resp.message ?? (resp.message ??
"Updates don't apply inside Docker — re-pull the image instead.") + "Updates don't apply from this dashboard.") +
cmd, cmd,
}); });
return; return;

View File

@ -1570,6 +1570,9 @@ export interface StatusResponse {
* Empty in loopback mode; empty + ``auth_required=true`` is a * Empty in loopback mode; empty + ``auth_required=true`` is a
* fail-closed state (the dashboard will refuse to bind). */ * fail-closed state (the dashboard will refuse to bind). */
auth_providers?: string[]; auth_providers?: string[];
/** False when the dashboard is running in a hosted/managed layout where
* updates are handled by the outer launcher instead of ``hermes update``. */
can_update_hermes?: boolean;
config_path: string; config_path: string;
config_version: number; config_version: number;
env_path: string; env_path: string;

View File

@ -386,6 +386,7 @@ export default function SystemPage() {
// ── Update check / apply ─────────────────────────────────────────── // ── Update check / apply ───────────────────────────────────────────
const checkForUpdate = useCallback( const checkForUpdate = useCallback(
async (force = false) => { async (force = false) => {
if (status?.can_update_hermes === false) return;
setCheckingUpdate(true); setCheckingUpdate(true);
try { try {
const info = await api.checkHermesUpdate(force); const info = await api.checkHermesUpdate(force);
@ -410,20 +411,27 @@ export default function SystemPage() {
setCheckingUpdate(false); setCheckingUpdate(false);
} }
}, },
[showToast], [showToast, status?.can_update_hermes],
); );
// Auto-check (cached) runs inside loadAll on mount; this is the // Auto-check (cached) runs inside loadAll on mount; this is the
// user-triggered forced re-check from the "Check for updates" button. // user-triggered forced re-check from the "Check for updates" button.
const applyUpdate = async () => { const applyUpdate = async () => {
setUpdateConfirmOpen(false); setUpdateConfirmOpen(false);
if (status?.can_update_hermes === false) {
showToast(
"Hermes updates are managed by the hosted agent service.",
"success",
);
return;
}
try { try {
const resp = await api.updateHermes(); const resp = await api.updateHermes();
if (!resp.ok && resp.error === "docker_update_unsupported") { if (!resp.ok) {
showToast( showToast(
resp.message ?? resp.message ??
"Updates don't apply inside Docker — re-pull the image instead.", "Updates don't apply from this dashboard.",
"error", "success",
); );
return; return;
} }
@ -503,6 +511,7 @@ export default function SystemPage() {
} }
const gatewayRunning = status?.gateway_running; const gatewayRunning = status?.gateway_running;
const canUpdateHermes = status?.can_update_hermes !== false;
const validEvents = hooks?.valid_events?.length const validEvents = hooks?.valid_events?.length
? hooks.valid_events ? hooks.valid_events
: HOOK_EVENTS_FALLBACK; : HOOK_EVENTS_FALLBACK;
@ -512,7 +521,7 @@ export default function SystemPage() {
<Toast toast={toast} /> <Toast toast={toast} />
<ConfirmDialog <ConfirmDialog
open={updateConfirmOpen} open={canUpdateHermes && updateConfirmOpen}
onCancel={() => setUpdateConfirmOpen(false)} onCancel={() => setUpdateConfirmOpen(false)}
onConfirm={() => void applyUpdate()} onConfirm={() => void applyUpdate()}
title="Update Hermes?" title="Update Hermes?"
@ -691,7 +700,8 @@ export default function SystemPage() {
<div className="text-xs uppercase tracking-wider text-muted-foreground">Hermes</div> <div className="text-xs uppercase tracking-wider text-muted-foreground">Hermes</div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span>v{stats?.hermes_version}</span> <span>v{stats?.hermes_version}</span>
{updateInfo && {canUpdateHermes &&
updateInfo &&
(updateInfo.update_available ? ( (updateInfo.update_available ? (
<Badge tone="warning"> <Badge tone="warning">
{updateInfo.behind && updateInfo.behind > 0 {updateInfo.behind && updateInfo.behind > 0
@ -751,45 +761,47 @@ export default function SystemPage() {
CPU / memory / disk metrics. CPU / memory / disk metrics.
</p> </p>
)} )}
<div className="mt-4 flex flex-wrap items-center gap-2 border-t border-border pt-4"> {canUpdateHermes && (
<Button <div className="mt-4 flex flex-wrap items-center gap-2 border-t border-border pt-4">
size="sm"
ghost
disabled={checkingUpdate}
prefix={
checkingUpdate ? (
<Spinner className="h-3.5 w-3.5" />
) : (
<RotateCw className="h-3.5 w-3.5" />
)
}
onClick={() => void checkForUpdate(true)}
>
Check for updates
</Button>
{updateInfo?.update_available && updateInfo.can_apply && (
<Button <Button
size="sm" size="sm"
prefix={<Download className="h-3.5 w-3.5" />} ghost
onClick={() => setUpdateConfirmOpen(true)} disabled={checkingUpdate}
prefix={
checkingUpdate ? (
<Spinner className="h-3.5 w-3.5" />
) : (
<RotateCw className="h-3.5 w-3.5" />
)
}
onClick={() => void checkForUpdate(true)}
> >
Update now Check for updates
</Button> </Button>
)} {updateInfo?.update_available && updateInfo.can_apply && (
{updateInfo && <Button
!updateInfo.can_apply && size="sm"
updateInfo.update_available && ( prefix={<Download className="h-3.5 w-3.5" />}
onClick={() => setUpdateConfirmOpen(true)}
>
Update now
</Button>
)}
{updateInfo &&
!updateInfo.can_apply &&
updateInfo.update_available && (
<span className="text-xs text-muted-foreground">
Update with{" "}
<span className="font-mono">{updateInfo.update_command}</span>
</span>
)}
{updateInfo?.message && !updateInfo.update_available && (
<span className="text-xs text-muted-foreground"> <span className="text-xs text-muted-foreground">
Update with{" "} {updateInfo.message}
<span className="font-mono">{updateInfo.update_command}</span>
</span> </span>
)} )}
{updateInfo?.message && !updateInfo.update_available && ( </div>
<span className="text-xs text-muted-foreground"> )}
{updateInfo.message}
</span>
)}
</div>
</CardContent> </CardContent>
</Card> </Card>
</section> </section>