From f9bec78c82498a0f90b77405337d7bd4b555f1b4 Mon Sep 17 00:00:00 2001 From: DungTT Date: Mon, 8 Jun 2026 09:27:55 +0700 Subject: [PATCH] fix UI client --- agent/app/core/command_runner.py | 42 +++++- agent/app/core/installer.py | 32 ++++- web-client/src/main.jsx | 175 ++++++++++++++----------- web-client/src/styles.css | 213 ++++++++++++++++++++----------- 4 files changed, 313 insertions(+), 149 deletions(-) diff --git a/agent/app/core/command_runner.py b/agent/app/core/command_runner.py index 8c18147..ded7c80 100644 --- a/agent/app/core/command_runner.py +++ b/agent/app/core/command_runner.py @@ -1,14 +1,40 @@ from __future__ import annotations +import os import subprocess +from collections.abc import Mapping from app.config import settings from app.storage.repository import Repository +def _tail_output(label: str, output: str, line_limit: int = 6) -> str: + lines = [line.strip() for line in output.splitlines() if line.strip()] + if not lines: + return "" + return f"{label}: {' | '.join(lines[-line_limit:])}" + + +def _command_output_summary(stdout: str, stderr: str) -> str: + parts = [ + part + for part in ( + _tail_output("stderr", stderr), + _tail_output("stdout", stdout), + ) + if part + ] + summary = " ; ".join(parts) + return summary[:1600] + + class CommandError(RuntimeError): def __init__(self, command: list[str], returncode: int, stdout: str, stderr: str) -> None: - super().__init__(f"Command failed with exit code {returncode}: {' '.join(command)}") + message = f"Command failed with exit code {returncode}: {' '.join(command)}" + output_summary = _command_output_summary(stdout, stderr) + if output_summary: + message = f"{message}. Last output: {output_summary}" + super().__init__(message) self.command = command self.returncode = returncode self.stdout = stdout @@ -20,15 +46,26 @@ class CommandRunner: self.repository = repository self.task_id = task_id - def run(self, command: list[str], timeout: int | None = None) -> subprocess.CompletedProcess[str]: + def run( + self, + command: list[str], + timeout: int | None = None, + env: Mapping[str, str] | None = None, + ) -> subprocess.CompletedProcess[str]: if self.task_id: self.repository.add_log(self.task_id, "debug", f"Running command: {' '.join(command)}") + command_env = os.environ.copy() + if env: + command_env.update(env) + try: result = subprocess.run( command, check=False, capture_output=True, + env=command_env, + stdin=subprocess.DEVNULL, text=True, timeout=timeout or settings.command_timeout_seconds, ) @@ -47,4 +84,3 @@ class CommandRunner: raise CommandError(command, result.returncode, result.stdout, result.stderr) return result - diff --git a/agent/app/core/installer.py b/agent/app/core/installer.py index 410dcea..260b0ea 100644 --- a/agent/app/core/installer.py +++ b/agent/app/core/installer.py @@ -5,6 +5,22 @@ from pathlib import Path from app.core.command_runner import CommandRunner +APT_DPKG_OPTIONS = [ + "-o", + "Dpkg::Use-Pty=0", + "-o", + "Dpkg::Options::=--force-confdef", + "-o", + "Dpkg::Options::=--force-confold", +] + +APT_NONINTERACTIVE_ENV = { + "DEBIAN_FRONTEND": "noninteractive", + "DEBCONF_NONINTERACTIVE_SEEN": "true", + "APT_LISTCHANGES_FRONTEND": "none", +} + + def _parse_deb_control_output(output: str) -> dict[str, str]: metadata: dict[str, str] = {} @@ -49,11 +65,23 @@ class DebInstaller: return metadata def install_deb(self, file_path: Path) -> None: - self.command_runner.run(["apt", "install", "-y", str(file_path)]) + self.command_runner.run( + [ + "apt-get", + *APT_DPKG_OPTIONS, + "install", + "--yes", + str(file_path), + ], + env=APT_NONINTERACTIVE_ENV, + ) def remove_package(self, package_name: str, purge: bool = False) -> None: action = "purge" if purge else "remove" - self.command_runner.run(["apt", action, "-y", package_name]) + self.command_runner.run( + ["apt-get", *APT_DPKG_OPTIONS, action, "--yes", package_name], + env=APT_NONINTERACTIVE_ENV, + ) def get_package_version(self, package_name: str) -> str | None: result = self.command_runner.run(["dpkg-query", "-W", "-f=${Version}", package_name]) diff --git a/web-client/src/main.jsx b/web-client/src/main.jsx index 48e5f62..46c786f 100644 --- a/web-client/src/main.jsx +++ b/web-client/src/main.jsx @@ -233,6 +233,7 @@ function App() { const [busyAction, setBusyAction] = useState(''); const [activeTask, setActiveTask] = useState(null); const [endpointDialogOpen, setEndpointDialogOpen] = useState(false); + const [agentDialogOpen, setAgentDialogOpen] = useState(false); const packageBaseUrl = settings.packageBaseUrl; const agentBaseUrl = settings.agentBaseUrl; @@ -300,15 +301,6 @@ function App() { ); }, [agentHealth?.agentVersion, latestAgentPackage?.version]); - const stats = useMemo(() => { - return { - available: apps.length, - installed: installedApps.length, - updates: mergedApps.filter((app) => app.canUpdate).length, - components: selectedManifest?.components?.length || selectedDetail?.packages?.length || 0 - }; - }, [apps.length, installedApps.length, mergedApps, selectedDetail, selectedManifest]); - const notify = useCallback((type, message) => { setToast({ id: Date.now(), type, message }); }, []); @@ -628,17 +620,18 @@ function App() { }, [toast]); useEffect(() => { - if (!endpointDialogOpen) return undefined; + if (!endpointDialogOpen && !agentDialogOpen) return undefined; function onKeyDown(event) { if (event.key === 'Escape') { - closeEndpointDialog(); + if (endpointDialogOpen) closeEndpointDialog(); + if (agentDialogOpen) setAgentDialogOpen(false); } } window.addEventListener('keydown', onKeyDown); return () => window.removeEventListener('keydown', onKeyDown); - }, [closeEndpointDialog, endpointDialogOpen]); + }, [agentDialogOpen, closeEndpointDialog, endpointDialogOpen]); const agentStatusTitle = !canUseAgentEndpoint ? (isClientWindows ? 'Remote endpoint needed' : 'Linux client required') @@ -706,6 +699,12 @@ function App() { {topbarAgentState}
+ setAgentDialogOpen(true)} + title={agentStatusTitle} + tone={agentStatusTone} + /> @@ -717,34 +716,6 @@ function App() {
-
-
-

Application catalog

-

Released apps from package server and install state on the selected Agent endpoint.

-
- {canShowAgentCommand && ( -
- - {isClientLinux && isLocalAgentEndpoint && ( - - - )} -
- )} -
- -
- - - - -
- {!canUseAgentEndpoint && (