import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { createRoot } from 'react-dom/client'; import { Activity, AlertCircle, Box, CheckCircle2, Clipboard, Cpu, Download, ExternalLink, HardDrive, Loader2, PackageCheck, Play, PlugZap, RefreshCcw, RotateCcw, Search, Server, Settings, ShieldCheck, Trash2, WifiOff, X, XCircle } from 'lucide-react'; import { DEFAULT_AGENT_BASE_URL, DEFAULT_PACKAGE_BASE_URL, fetchAgentHealth, fetchAgentSystemInfo, fetchApplicationDetail, fetchApplicationManifest, fetchInstalledApps, fetchLatestAgentPackage, fetchPackageApps, fetchTaskComponents, fetchTaskLogs, fetchTaskStatus, getAppOpenUrl, joinUrl, normalizeUrl, queueInstall, queueRemove, queueUpdate } from './services/api.js'; import './styles.css'; const SETTINGS_KEY = 'robot-installer-client-settings'; const TERMINAL_TASK_STATUSES = new Set(['success', 'failed', 'cancelled']); const TASK_STATUS_TONES = { queued: 'info', running: 'info', success: 'success', failed: 'danger', cancelled: 'warning' }; const TASK_ACTION_LABELS = { install: 'Installing', update: 'Updating', remove: 'Removing' }; const AGENT_VERSION_COLLATOR = new Intl.Collator('en', { numeric: true, sensitivity: 'base' }); function readSettings() { try { const parsed = JSON.parse(window.localStorage.getItem(SETTINGS_KEY) || '{}'); return { packageBaseUrl: normalizeUrl(parsed.packageBaseUrl || DEFAULT_PACKAGE_BASE_URL), agentBaseUrl: normalizeUrl(parsed.agentBaseUrl || DEFAULT_AGENT_BASE_URL) }; } catch { return { packageBaseUrl: DEFAULT_PACKAGE_BASE_URL, agentBaseUrl: DEFAULT_AGENT_BASE_URL }; } } function saveSettings(settings) { window.localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings)); } function getErrorMessage(error) { return error instanceof Error ? error.message : String(error || 'Có lỗi xảy ra'); } function copyTextFallback(text) { const textarea = document.createElement('textarea'); textarea.value = text; textarea.setAttribute('readonly', ''); textarea.style.position = 'fixed'; textarea.style.top = '-1000px'; textarea.style.opacity = '0'; document.body.appendChild(textarea); textarea.focus(); textarea.select(); textarea.setSelectionRange(0, textarea.value.length); try { return document.execCommand('copy'); } finally { document.body.removeChild(textarea); } } function isTaskActive(task) { return Boolean(task?.taskId && !TERMINAL_TASK_STATUSES.has(task.status)); } function clampProgress(value) { return Math.max(0, Math.min(100, Number(value) || 0)); } function statusBadgeClass(status) { const tone = TASK_STATUS_TONES[status] || 'info'; return `badge badge-${tone}`; } function formatTaskTime(value) { if (!value) return '--'; const parsed = new Date(value); if (Number.isNaN(parsed.getTime())) return value; return parsed.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' }); } function compareAgentVersions(currentVersion, latestVersion) { const current = String(currentVersion || '').trim(); const latest = String(latestVersion || '').trim(); if (!current && !latest) return 0; if (!current) return -1; if (!latest) return 1; return AGENT_VERSION_COLLATOR.compare(current, latest); } const CLIENT_OS_LABELS = { android: 'Android', linux: 'Linux', macos: 'macOS', unknown: 'unknown OS', windows: 'Windows' }; function detectClientOs() { if (typeof navigator === 'undefined') return 'unknown'; const ua = String(navigator.userAgent || '').toLowerCase(); const platform = [ navigator.userAgentData?.platform, navigator.platform ].filter(Boolean).join(' ').toLowerCase(); if (ua.includes('android') || platform.includes('android')) return 'android'; if (platform.includes('linux') || ua.includes('linux')) return 'linux'; if (platform.includes('win') || ua.includes('windows')) return 'windows'; if (platform.includes('mac') || ua.includes('mac os')) return 'macos'; return 'unknown'; } function getClientOsLabel(os) { return CLIENT_OS_LABELS[os] || CLIENT_OS_LABELS.unknown; } function parseUrlLike(value) { const text = normalizeUrl(value); if (!text) return null; const urlText = /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(text) ? text : `http://${text}`; try { return new URL(urlText); } catch { return null; } } function isLoopbackHostname(hostname) { const host = String(hostname || '').trim().toLowerCase().replace(/^\[|\]$/g, ''); return Boolean( host === 'localhost' || host.endsWith('.localhost') || host === '::1' || host === '0.0.0.0' || /^127(?:\.\d{1,3}){0,3}$/.test(host) ); } function isLoopbackAgentEndpoint(agentBaseUrl) { const parsed = parseUrlLike(agentBaseUrl); return !parsed?.hostname || isLoopbackHostname(parsed.hostname); } function resolveTargetOpenUrl(openUrl, agentBaseUrl, isRemoteAgentEndpoint) { if (!isRemoteAgentEndpoint) return openUrl; const parsedOpenUrl = parseUrlLike(openUrl); const parsedAgentUrl = parseUrlLike(agentBaseUrl); if (!parsedOpenUrl || !parsedAgentUrl || !isLoopbackHostname(parsedOpenUrl.hostname)) { return openUrl; } parsedOpenUrl.hostname = parsedAgentUrl.hostname; return parsedOpenUrl.toString(); } function App() { const [settings, setSettings] = useState(readSettings); const [draftSettings, setDraftSettings] = useState(settings); const [apps, setApps] = useState([]); const [installedApps, setInstalledApps] = useState([]); const [latestAgentPackage, setLatestAgentPackage] = useState(null); const [agentHealth, setAgentHealth] = useState(null); const [systemInfo, setSystemInfo] = useState(null); const [packageStatus, setPackageStatus] = useState({ state: 'idle', message: '' }); const [agentStatus, setAgentStatus] = useState({ state: 'idle', message: '' }); const [selectedAppId, setSelectedAppId] = useState(''); const [selectedDetail, setSelectedDetail] = useState(null); const [selectedManifest, setSelectedManifest] = useState(null); const [detailStatus, setDetailStatus] = useState({ state: 'idle', message: '' }); const [query, setQuery] = useState(''); const [filter, setFilter] = useState('all'); const [toast, setToast] = useState(null); 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; const installCommand = `curl -fsSL ${joinUrl(packageBaseUrl, '/install-agent.sh')} | sudo bash`; const agentCommand = latestAgentPackage?.installCommand || installCommand; const clientOs = useMemo(() => detectClientOs(), []); const isClientLinux = clientOs === 'linux'; const isClientWindows = clientOs === 'windows'; const isLocalAgentEndpoint = useMemo(() => isLoopbackAgentEndpoint(agentBaseUrl), [agentBaseUrl]); const isRemoteAgentEndpoint = !isLocalAgentEndpoint; const canUseAgentEndpoint = isRemoteAgentEndpoint || isClientLinux; const canManageApps = canUseAgentEndpoint; const canShowAgentCommand = isClientLinux || isClientWindows || isRemoteAgentEndpoint; const clientOsLabel = getClientOsLabel(clientOs); const agentTargetLabel = isRemoteAgentEndpoint ? 'Remote Ubuntu Agent' : 'Local Agent'; const agentTargetMachine = isRemoteAgentEndpoint ? 'remote Ubuntu server' : 'local machine'; const installedByAppId = useMemo(() => { return new Map(installedApps.map((app) => [app.appId, app])); }, [installedApps]); const mergedApps = useMemo(() => { return apps.map((app) => { const installed = installedByAppId.get(app.appId) || installedByAppId.get(app.appCode); const isInstalled = Boolean(installed); const canUpdate = Boolean(isInstalled && installed.version && installed.version !== app.version); return { ...app, installed, localStatus: canUpdate ? 'update' : (isInstalled ? 'installed' : 'available'), canUpdate }; }); }, [apps, installedByAppId]); const filteredApps = useMemo(() => { const needle = query.trim().toLowerCase(); return mergedApps.filter((app) => { const matchesQuery = !needle || [ app.appId, app.appCode, app.appName, app.version, app.status ].join(' ').toLowerCase().includes(needle); if (!matchesQuery) return false; if (filter === 'installed') return app.localStatus === 'installed' || app.localStatus === 'update'; if (filter === 'updates') return app.localStatus === 'update'; if (filter === 'available') return app.localStatus === 'available'; return true; }); }, [filter, mergedApps, query]); const selectedApp = useMemo(() => { return mergedApps.find((app) => app.appId === selectedAppId) || mergedApps[0] || null; }, [mergedApps, selectedAppId]); const agentNeedsUpdate = useMemo(() => { return Boolean( agentHealth?.agentVersion && latestAgentPackage?.version && compareAgentVersions(agentHealth.agentVersion, latestAgentPackage.version) < 0 ); }, [agentHealth?.agentVersion, latestAgentPackage?.version]); const notify = useCallback((type, message) => { setToast({ id: Date.now(), type, message }); }, []); const refreshPackage = useCallback(async () => { setPackageStatus({ state: 'loading', message: 'Đang tải app từ package server' }); try { const [nextApps, nextAgentPackage] = await Promise.all([ fetchPackageApps(packageBaseUrl), fetchLatestAgentPackage(packageBaseUrl).catch(() => null) ]); setApps(nextApps); setLatestAgentPackage(nextAgentPackage); const agentNote = nextAgentPackage?.version ? ` · Agent ${nextAgentPackage.version}` : ''; setPackageStatus({ state: 'success', message: `${nextApps.length} app released${agentNote}` }); return nextApps; } catch (error) { setPackageStatus({ state: 'danger', message: getErrorMessage(error) }); setApps([]); setLatestAgentPackage(null); return []; } }, [packageBaseUrl]); const refreshAgent = useCallback(async () => { if (!canUseAgentEndpoint) { setAgentHealth(null); setSystemInfo(null); setInstalledApps([]); setAgentStatus({ state: 'warning', message: isClientWindows ? 'Local endpoint points to this Windows machine. Use a remote Ubuntu Agent endpoint.' : `Local endpoint requires Linux. Current client is ${clientOsLabel}.` }); return false; } setAgentStatus({ state: 'loading', message: `Checking ${agentTargetLabel}` }); try { const health = await fetchAgentHealth(agentBaseUrl); setAgentHealth(health); setAgentStatus({ state: 'success', message: `${health.hostname || agentTargetLabel} online` }); const [info, installed] = await Promise.all([ fetchAgentSystemInfo(agentBaseUrl).catch(() => null), fetchInstalledApps(agentBaseUrl) ]); setSystemInfo(info); setInstalledApps(installed); return true; } catch (error) { setAgentHealth(null); setSystemInfo(null); setInstalledApps([]); setAgentStatus({ state: 'danger', message: getErrorMessage(error) }); return false; } }, [agentBaseUrl, agentTargetLabel, canUseAgentEndpoint, clientOsLabel, isClientWindows]); const refreshAll = useCallback(async () => { await Promise.all([refreshPackage(), refreshAgent()]); }, [refreshAgent, refreshPackage]); const loadSelectedDetail = useCallback(async (app) => { if (!app) { setSelectedDetail(null); setSelectedManifest(null); return; } setDetailStatus({ state: 'loading', message: 'Đang tải manifest' }); try { const [detail, manifest] = await Promise.all([ fetchApplicationDetail(packageBaseUrl, app.appId).catch(() => null), fetchApplicationManifest(packageBaseUrl, app.appId, app.version).catch(() => null) ]); setSelectedDetail(detail); setSelectedManifest(manifest); setDetailStatus({ state: 'success', message: manifest ? 'Manifest sẵn sàng' : 'Đã tải app detail' }); } catch (error) { setSelectedDetail(null); setSelectedManifest(null); setDetailStatus({ state: 'danger', message: getErrorMessage(error) }); } }, [packageBaseUrl]); const loadTaskSnapshot = useCallback(async (taskId) => { try { const [nextTask, logs, components] = await Promise.all([ fetchTaskStatus(agentBaseUrl, taskId), fetchTaskLogs(agentBaseUrl, taskId).catch(() => []), fetchTaskComponents(agentBaseUrl, taskId).catch(() => []) ]); const snapshot = { ...nextTask, logs, components, pollError: '' }; setActiveTask((current) => { if (!current || current.taskId !== taskId) return current; return { ...current, ...snapshot }; }); if (TERMINAL_TASK_STATUSES.has(snapshot.status)) { await refreshAgent(); } return snapshot; } catch (error) { const message = getErrorMessage(error); setActiveTask((current) => { if (!current || current.taskId !== taskId) return current; return { ...current, pollError: message }; }); return null; } }, [agentBaseUrl, refreshAgent]); const startTask = useCallback((queuedTask, action, app) => { const queuedAt = new Date().toISOString(); setActiveTask({ taskId: queuedTask.taskId, action, appId: app.appId, appName: app.appName, status: queuedTask.status || 'queued', progress: 0, currentStep: 'queued', logs: [ { time: queuedAt, level: 'info', message: `Task ${queuedTask.taskId} queued` } ], components: [], pollError: '', queuedAt }); }, []); const startPreflightFailedTask = useCallback((action, app, message) => { const failedAt = new Date().toISOString(); const taskId = `preflight_${Date.now()}`; setActiveTask({ taskId, action, appId: app.appId, appName: app.appName, status: 'failed', progress: 5, currentStep: 'package preflight failed', errorMessage: message, logs: [ { time: failedAt, level: 'error', message } ], components: [], pollError: '', localOnly: true, queuedAt: failedAt, finishedAt: failedAt }); }, []); const runAppAction = useCallback(async (action, app) => { if (!canManageApps) { notify('warning', 'Local Agent endpoint can install only from a Linux browser machine. Use a remote Ubuntu Agent endpoint.'); return; } if (!agentHealth) { notify('warning', `${agentTargetLabel} is offline. Check the endpoint and press Retry.`); return; } if (action === 'remove' && !window.confirm(`Remove ${app.appName} from ${agentTargetMachine}?`)) { return; } const key = `${action}:${app.appId}`; setBusyAction(key); try { let queuedTask; if (action === 'install') { const manifest = await fetchApplicationManifest(packageBaseUrl, app.appId, app.version); setSelectedManifest(manifest); setDetailStatus({ state: 'success', message: 'Manifest ready' }); queuedTask = await queueInstall(agentBaseUrl, app); } else if (action === 'update') { const manifest = await fetchApplicationManifest(packageBaseUrl, app.appId, app.version); setSelectedManifest(manifest); setDetailStatus({ state: 'success', message: 'Manifest ready' }); queuedTask = await queueUpdate(agentBaseUrl, app, app.installed); } else { queuedTask = await queueRemove(agentBaseUrl, app); } startTask(queuedTask, action, app); notify('success', `Đã queue task ${queuedTask.taskId}`); } catch (error) { const message = getErrorMessage(error); if (action === 'install' || action === 'update') { setSelectedManifest(null); setDetailStatus({ state: 'danger', message }); startPreflightFailedTask(action, app, message); } notify('failure', message); } finally { setBusyAction(''); } }, [agentBaseUrl, agentHealth, agentTargetLabel, agentTargetMachine, canManageApps, notify, packageBaseUrl, startPreflightFailedTask, startTask]); const openEndpointDialog = useCallback(() => { setDraftSettings(settings); setEndpointDialogOpen(true); }, [settings]); const closeEndpointDialog = useCallback(() => { setDraftSettings(settings); setEndpointDialogOpen(false); }, [settings]); const applySettings = useCallback(() => { const nextSettings = { packageBaseUrl: normalizeUrl(draftSettings.packageBaseUrl || DEFAULT_PACKAGE_BASE_URL), agentBaseUrl: normalizeUrl(draftSettings.agentBaseUrl || DEFAULT_AGENT_BASE_URL) }; setSettings(nextSettings); setDraftSettings(nextSettings); saveSettings(nextSettings); setEndpointDialogOpen(false); notify('info', 'Đã cập nhật endpoint test'); }, [draftSettings, notify]); const copyInstallCommand = useCallback(async () => { if (!canShowAgentCommand) { notify('warning', 'Agent command is hidden for this client OS.'); return; } if (!navigator.clipboard?.writeText) { if (copyTextFallback(agentCommand)) { notify('success', agentNeedsUpdate ? 'Da copy lenh update Agent' : 'Da copy lenh cai Agent'); return; } window.prompt('Copy Agent command', agentCommand); notify('warning', 'Browser dang chan copy tu dong. Hay copy lenh trong popup.'); return; } try { await navigator.clipboard.writeText(agentCommand); notify('success', agentNeedsUpdate ? 'Đã copy lệnh update Agent' : 'Đã copy lệnh cài Agent'); } catch { notify('warning', 'Không thể copy tự động trong browser này'); } }, [agentCommand, agentNeedsUpdate, canShowAgentCommand, notify]); useEffect(() => { refreshAll(); }, [refreshAll]); useEffect(() => { if (!selectedApp) { setSelectedDetail(null); setSelectedManifest(null); return; } setSelectedAppId(selectedApp.appId); loadSelectedDetail(selectedApp); }, [loadSelectedDetail, selectedApp?.appId]); useEffect(() => { if (!activeTask?.taskId || TERMINAL_TASK_STATUSES.has(activeTask.status)) return undefined; let disposed = false; let terminalNotified = false; async function poll() { const nextTask = await loadTaskSnapshot(activeTask.taskId); if (disposed || !nextTask) return; if (TERMINAL_TASK_STATUSES.has(nextTask.status)) { window.clearInterval(timer); if (!terminalNotified) { terminalNotified = true; if (nextTask.status === 'success') { notify('success', `${activeTask.appName || 'Task'} completed`); } else if (nextTask.status === 'failed') { notify('failure', nextTask.errorMessage || `Task ${nextTask.taskId} failed`); } else { notify('warning', `Task ${nextTask.taskId} ${nextTask.status}`); } } } } poll(); const timer = window.setInterval(poll, 1200); return () => { disposed = true; window.clearInterval(timer); }; }, [activeTask?.action, activeTask?.appName, activeTask?.status, activeTask?.taskId, loadTaskSnapshot, notify]); useEffect(() => { if (!toast) return undefined; const timer = window.setTimeout(() => setToast(null), 3200); return () => window.clearTimeout(timer); }, [toast]); useEffect(() => { if (!endpointDialogOpen && !agentDialogOpen) return undefined; function onKeyDown(event) { if (event.key === 'Escape') { if (endpointDialogOpen) closeEndpointDialog(); if (agentDialogOpen) setAgentDialogOpen(false); } } window.addEventListener('keydown', onKeyDown); return () => window.removeEventListener('keydown', onKeyDown); }, [agentDialogOpen, closeEndpointDialog, endpointDialogOpen]); const agentStatusTitle = !canUseAgentEndpoint ? (isClientWindows ? 'Remote endpoint needed' : 'Linux client required') : (agentNeedsUpdate ? 'Agent update available' : (agentHealth ? 'Agent online' : 'Agent offline')); const agentStatusDetail = !canUseAgentEndpoint ? (isClientWindows ? 'Change endpoint to Ubuntu server IP' : `${clientOsLabel} detected`) : (agentHealth ? `${agentHealth.hostname || agentTargetLabel} · ${agentHealth.agentVersion || '-'}${agentNeedsUpdate ? ` -> ${latestAgentPackage.version}` : ''}` : agentBaseUrl); const agentStatusTone = !canUseAgentEndpoint ? 'warning' : (agentNeedsUpdate ? 'warning' : (agentHealth ? 'success' : (agentStatus.state === 'loading' ? 'info' : 'danger'))); const topbarAgentState = !canUseAgentEndpoint ? 'Use Ubuntu Agent endpoint' : (agentHealth ? `Ready on ${agentTargetMachine}` : `Waiting for ${agentTargetLabel}`); return (
robot.installer {topbarAgentState}
setAgentDialogOpen(true)} title={agentStatusTitle} tone={agentStatusTone} />
{!canUseAgentEndpoint && ( )} {canUseAgentEndpoint && !agentHealth && (
)} {canUseAgentEndpoint && agentNeedsUpdate && (
)}
{packageStatus.state === 'loading' && ( )} {packageStatus.state === 'danger' && ( )} {packageStatus.state !== 'loading' && packageStatus.state !== 'danger' && filteredApps.length === 0 && ( )} {filteredApps.map((app) => { const rowTask = activeTask?.appId === app.appId ? activeTask : null; const rowTaskBusy = isTaskActive(rowTask); const installBusy = busyAction === `install:${app.appId}` || (rowTaskBusy && rowTask.action === 'install'); const updateBusy = busyAction === `update:${app.appId}` || (rowTaskBusy && rowTask.action === 'update'); const removeBusy = busyAction === `remove:${app.appId}` || (rowTaskBusy && rowTask.action === 'remove'); const appOpenUrl = app.installed ? getAppOpenUrl({ ...app, openUrl: app.openUrl || app.installed.openUrl }) : ''; const openUrl = resolveTargetOpenUrl(appOpenUrl, agentBaseUrl, isRemoteAgentEndpoint); return ( setSelectedAppId(app.appId)} > ); })}
Application Released Local Packages Actions
Đang tải danh sách app...
{packageStatus.message}
Chưa có app phù hợp bộ lọc.
{app.appCode || app.appId} {app.version} {app.packageCount || 0}
{canManageApps && !app.installed && ( )} {canManageApps && app.installed && app.canUpdate && ( )} {canManageApps && app.installed && openUrl && ( event.stopPropagation()} > )} {canManageApps && app.installed && ( )}
{filteredApps.length} / {mergedApps.length} apps {packageBaseUrl}
{toast && } {agentDialogOpen && ( setAgentDialogOpen(false)} onCopyUpdate={copyInstallCommand} showAgentActions={canShowAgentCommand} status={agentStatus} systemInfo={systemInfo} title={agentTargetLabel} /> )} {endpointDialogOpen && ( )}
); } function AgentStatusButton({ title, detail, tone, onClick }) { const Icon = tone === 'success' ? CheckCircle2 : tone === 'danger' ? XCircle : tone === 'warning' ? AlertCircle : Loader2; return ( ); } function AgentDialog(props) { return (
); } function EndpointDialog({ draftSettings, onApply, onCancel, onChange }) { return (
{ event.preventDefault(); onApply(); }} >

Endpoint settings

Changes only apply after you press Apply.

); } function StatusBox({ icon: Icon, title, detail, tone }) { return (
{title} {detail}
); } function ClientOsNotice({ agentCommand, canShowCommand, isWindows, onCopyCommand, osLabel }) { return (
); } function LocalStatus({ app, task }) { if (isTaskActive(task)) { return ( {TASK_ACTION_LABELS[task.action] || task.status} {clampProgress(task.progress)}% ); } if (app.localStatus === 'update') { return ( Update {app.installed.version} ); } if (app.localStatus === 'installed') { return ( Installed {app.installed.version} ); } return Available; } function TaskPanel({ task, onClear, onRefresh }) { const progress = clampProgress(task.progress); const statusTone = TASK_STATUS_TONES[task.status] || 'info'; const components = task.components || []; const logs = task.logs || []; const visibleLogs = logs.slice(-6); const canClear = TERMINAL_TASK_STATUSES.has(task.status); const canRefresh = !task.localOnly; return (

Task monitor

{task.taskId}

{task.status || 'queued'} {canClear && ( )}
Action
{task.action || task.type || '-'}
App
{task.appName || task.appId || '-'}
Step
{task.currentStep || '-'}
{(task.errorMessage || task.pollError) && (
Error
{task.errorMessage || task.pollError}
)}
{components.map((component) => (
{component.componentId} {component.currentStep || component.type || '-'}
{component.progress}%
))} {!components.length && (
Waiting for component details...
)}
{visibleLogs.map((log, index) => (
{formatTaskTime(log.time)} {log.level}

{log.message}

))} {!visibleLogs.length && (
Waiting for task logs...
)}
); } function AgentPanel({ health, systemInfo, status, title, endpoint, latestAgentPackage, needsUpdate, onClose, onCopyUpdate, showAgentActions }) { const statusTone = needsUpdate ? 'warning' : health ? 'success' : status.state === 'loading' ? 'info' : status.state === 'warning' ? 'warning' : 'danger'; const statusLabel = needsUpdate ? 'Update available' : health ? 'Online' : status.state === 'loading' ? 'Checking' : status.state === 'warning' ? 'Warning' : 'Offline'; return (

{title || 'Agent'}

{status.message || endpoint || '127.0.0.1:5010'}

{needsUpdate ?
Status
{statusLabel}
Endpoint
{endpoint || '127.0.0.1:5010'}
Version
{health?.agentVersion || '-'} {needsUpdate && Update}
Latest
{latestAgentPackage?.version || '-'}
Host
{health?.hostname || '-'}
OS
{systemInfo?.os || health?.os || '-'}
Arch
{systemInfo?.architecture || health?.architecture || '-'}
{showAgentActions && (
{needsUpdate && latestAgentPackage?.downloadUrl && ( )}
)}
); } function AppDetailPanel({ app, detail, manifest, status, packageBaseUrl }) { const packages = detail?.packages || []; const components = manifest?.components || []; return (

{app?.appName || 'App detail'}

{app?.appCode || app?.appId || status.message || packageBaseUrl}

{status.state === 'loading' ?
{app ? ( <>
Released
{app.version}
Status
{app.status || 'Released'}
Local
{app.installed ? `${app.installed.version} · ${app.installed.status || 'installed'}` : 'Not installed'}
{status.state === 'danger' && (
{status.message}
)} {(components.length ? components : packages).map((item) => (
{item.componentId || item.code || item.name} {item.packageName || item.selectedVersion || item.type}
{item.version || item.selectedVersion || item.type || 'pkg'}
))} {!components.length && !packages.length && (
{status.message || 'Chưa có component.'}
)}
) : (
Chọn app để xem manifest.
)}
); } function Toast({ toast }) { const tone = toast.type === 'failure' ? 'danger' : toast.type; const Icon = tone === 'danger' ? AlertCircle : tone === 'success' ? CheckCircle2 : Activity; return (
); } createRoot(document.getElementById('root')).render();