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 (
{!canUseAgentEndpoint && (
)}
{canUseAgentEndpoint && !agentHealth && (
{agentTargetLabel} is offline
{agentCommand}
Copy
)}
{canUseAgentEndpoint && agentNeedsUpdate && (
Agent {latestAgentPackage.version} is ready
{agentCommand}
Copy
)}
Application
Released
Local
Packages
Actions
{packageStatus.state === 'loading' && (
Đang tải danh sách app...
)}
{packageStatus.state === 'danger' && (
{packageStatus.message}
)}
{packageStatus.state !== 'loading' && packageStatus.state !== 'danger' && filteredApps.length === 0 && (
Chưa có app phù hợp bộ lọc.
)}
{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)}
>
setSelectedAppId(app.appId)}>
{app.appName}
{app.appCode || app.appId}
{app.version}
{app.packageCount || 0}
{canManageApps && !app.installed && (
{
event.stopPropagation();
runAppAction('install', app);
}}
>
{installBusy ? : }
Install
)}
{canManageApps && app.installed && app.canUpdate && (
{
event.stopPropagation();
runAppAction('update', app);
}}
>
{updateBusy ? : }
Update
)}
{canManageApps && app.installed && openUrl && (
event.stopPropagation()}
>
Open App
)}
{canManageApps && app.installed && (
{
event.stopPropagation();
runAppAction('remove', app);
}}
>
{removeBusy ? : }
)}
);
})}
{filteredApps.length} / {mergedApps.length} apps
{packageBaseUrl}
{activeTask && (
setActiveTask(null)}
onRefresh={() => loadTaskSnapshot(activeTask.taskId)}
/>
)}
{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 (
{title}
{detail}
);
}
function AgentDialog(props) {
return (
);
}
function EndpointDialog({ draftSettings, onApply, onCancel, onChange }) {
return (
);
}
function StatusBox({ icon: Icon, title, detail, tone }) {
return (
);
}
function ClientOsNotice({ agentCommand, canShowCommand, isWindows, onCopyCommand, osLabel }) {
return (
{isWindows ? 'Remote Ubuntu endpoint needed' : 'Local Agent requires Linux'}
{isWindows
? 'The endpoint is localhost, so it points at this Windows machine. Change it to http://:5010 or run the command below over Ubuntu SSH.'
: `Current client is ${osLabel}. Use a remote Ubuntu Agent endpoint or open the Web Client from Linux.`}
{canShowCommand &&
{agentCommand}}
{canShowCommand && (
Copy
)}
);
}
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 ?
: health ?
:
}
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 || '-'}
{systemInfo?.kernel || 'kernel -'}
{systemInfo?.diskFree || 'disk -'}
{showAgentActions && (
{needsUpdate ? 'Copy update command' : 'Copy Agent command'}
{needsUpdate && latestAgentPackage?.downloadUrl && (
Latest .deb
)}
)}
);
}
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'}
Components
{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 (
{toast.message}
);
}
createRoot(document.getElementById('root')).render( );