UI cảnh báo cho máy không phải linux

This commit is contained in:
2026-06-03 15:29:56 +07:00
parent c01d9c7e40
commit ce3b3c900a
2 changed files with 111 additions and 25 deletions

View File

@@ -141,6 +141,34 @@ function compareAgentVersions(currentVersion, latestVersion) {
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 App() {
const [settings, setSettings] = useState(readSettings);
const [draftSettings, setDraftSettings] = useState(settings);
@@ -166,6 +194,9 @@ function App() {
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 clientOsLabel = getClientOsLabel(clientOs);
const installedByAppId = useMemo(() => {
return new Map(installedApps.map((app) => [app.appId, app]));
@@ -251,6 +282,17 @@ function App() {
}, [packageBaseUrl]);
const refreshAgent = useCallback(async () => {
if (!isClientLinux) {
setAgentHealth(null);
setSystemInfo(null);
setInstalledApps([]);
setAgentStatus({
state: 'warning',
message: `Current client is ${clientOsLabel}. Web Client only supports Linux.`
});
return false;
}
setAgentStatus({ state: 'loading', message: 'Đang kiểm tra Agent local' });
try {
const health = await fetchAgentHealth(agentBaseUrl);
@@ -271,7 +313,7 @@ function App() {
setAgentStatus({ state: 'danger', message: getErrorMessage(error) });
return false;
}
}, [agentBaseUrl]);
}, [agentBaseUrl, clientOsLabel, isClientLinux]);
const refreshAll = useCallback(async () => {
await Promise.all([refreshPackage(), refreshAgent()]);
@@ -385,6 +427,11 @@ function App() {
}, []);
const runAppAction = useCallback(async (action, app) => {
if (!isClientLinux) {
notify('warning', 'Web Client chỉ hỗ trợ cài đặt trên máy Linux.');
return;
}
if (!agentHealth) {
notify('warning', 'Agent local đang offline. Cài Agent rồi bấm Retry.');
return;
@@ -425,7 +472,7 @@ function App() {
} finally {
setBusyAction('');
}
}, [agentBaseUrl, agentHealth, notify, packageBaseUrl, startPreflightFailedTask, startTask]);
}, [agentBaseUrl, agentHealth, isClientLinux, notify, packageBaseUrl, startPreflightFailedTask, startTask]);
const openEndpointDialog = useCallback(() => {
setDraftSettings(settings);
@@ -450,6 +497,11 @@ function App() {
}, [draftSettings, notify]);
const copyInstallCommand = useCallback(async () => {
if (!isClientLinux) {
notify('warning', 'Lệnh cài Agent chỉ hiển thị trên máy Linux.');
return;
}
if (!navigator.clipboard?.writeText) {
if (copyTextFallback(agentCommand)) {
notify('success', agentNeedsUpdate ? 'Da copy lenh update Agent' : 'Da copy lenh cai Agent');
@@ -467,7 +519,7 @@ function App() {
} catch {
notify('warning', 'Không thể copy tự động trong browser này');
}
}, [agentCommand, agentNeedsUpdate, notify]);
}, [agentCommand, agentNeedsUpdate, isClientLinux, notify]);
useEffect(() => {
refreshAll();
@@ -551,9 +603,9 @@ function App() {
<div className="nav-label">Runtime</div>
<StatusBox
icon={agentHealth ? PlugZap : WifiOff}
title={agentNeedsUpdate ? 'Agent update available' : (agentHealth ? 'Agent online' : 'Agent offline')}
detail={agentHealth ? `${agentHealth.hostname || 'localhost'} · ${agentHealth.agentVersion || '-'}${agentNeedsUpdate ? ` -> ${latestAgentPackage.version}` : ''}` : '127.0.0.1:5010'}
tone={agentNeedsUpdate ? 'warning' : (agentHealth ? 'success' : 'danger')}
title={!isClientLinux ? 'Linux client required' : (agentNeedsUpdate ? 'Agent update available' : (agentHealth ? 'Agent online' : 'Agent offline'))}
detail={!isClientLinux ? `${clientOsLabel} detected` : (agentHealth ? `${agentHealth.hostname || 'localhost'} · ${agentHealth.agentVersion || '-'}${agentNeedsUpdate ? ` -> ${latestAgentPackage.version}` : ''}` : '127.0.0.1:5010')}
tone={!isClientLinux ? 'warning' : (agentNeedsUpdate ? 'warning' : (agentHealth ? 'success' : 'danger'))}
/>
<StatusBox
icon={Server}
@@ -584,7 +636,7 @@ function App() {
<header className="topbar">
<div className="topbar-title">
<span>robot.installer</span>
<strong>{agentHealth ? 'Ready for install' : 'Waiting for Agent'}</strong>
<strong>{!isClientLinux ? 'Linux client required' : (agentHealth ? 'Ready for install' : 'Waiting for Agent')}</strong>
</div>
<div className="topbar-actions">
<a className="icon-button" href={joinUrl(packageBaseUrl, '/api/apps')} target="_blank" rel="noreferrer" title="Open package API">
@@ -603,16 +655,18 @@ function App() {
<h1>Application catalog</h1>
<p>Released apps từ package server trạng thái cài đặt trên máy local.</p>
</div>
<div className="page-actions">
<button className="btn btn-secondary" type="button" onClick={copyInstallCommand}>
<Clipboard size={15} aria-hidden="true" />
{agentNeedsUpdate ? 'Copy Agent update' : 'Copy Agent command'}
</button>
<a className="btn btn-primary" href={joinUrl(packageBaseUrl, '/install-agent.sh')} target="_blank" rel="noreferrer">
<Download size={15} aria-hidden="true" />
{agentNeedsUpdate ? 'update-agent.sh' : 'install-agent.sh'}
</a>
</div>
{isClientLinux && (
<div className="page-actions">
<button className="btn btn-secondary" type="button" onClick={copyInstallCommand}>
<Clipboard size={15} aria-hidden="true" />
{agentNeedsUpdate ? 'Copy Agent update' : 'Copy Agent command'}
</button>
<a className="btn btn-primary" href={joinUrl(packageBaseUrl, '/install-agent.sh')} target="_blank" rel="noreferrer">
<Download size={15} aria-hidden="true" />
{agentNeedsUpdate ? 'update-agent.sh' : 'install-agent.sh'}
</a>
</div>
)}
</div>
<div className="dashboard-stats">
@@ -622,7 +676,11 @@ function App() {
<MetricCard label="Components" value={stats.components} note={selectedApp?.appCode || selectedApp?.appId || 'selected app'} />
</div>
{!agentHealth && (
{!isClientLinux && (
<ClientOsNotice osLabel={clientOsLabel} />
)}
{isClientLinux && !agentHealth && (
<div className="offline-banner">
<AlertCircle size={19} aria-hidden="true" />
<div>
@@ -636,7 +694,7 @@ function App() {
</div>
)}
{agentNeedsUpdate && (
{isClientLinux && agentNeedsUpdate && (
<div className="offline-banner agent-update-banner">
<AlertCircle size={19} aria-hidden="true" />
<div>
@@ -733,7 +791,7 @@ function App() {
<td>{app.packageCount || 0}</td>
<td className="action-col">
<div className="action-group">
{!app.installed && (
{isClientLinux && !app.installed && (
<button
className="btn btn-primary compact"
type="button"
@@ -747,7 +805,7 @@ function App() {
Install
</button>
)}
{app.installed && app.canUpdate && (
{isClientLinux && app.installed && app.canUpdate && (
<button
className="btn btn-warning compact"
type="button"
@@ -761,7 +819,7 @@ function App() {
Update
</button>
)}
{app.installed && openUrl && (
{isClientLinux && app.installed && openUrl && (
<a
className="btn btn-secondary compact"
href={openUrl}
@@ -774,7 +832,7 @@ function App() {
Open App
</a>
)}
{app.installed && (
{isClientLinux && app.installed && (
<button
className="icon-button danger"
type="button"
@@ -810,6 +868,7 @@ function App() {
latestAgentPackage={latestAgentPackage}
needsUpdate={agentNeedsUpdate}
onCopyUpdate={copyInstallCommand}
showAgentActions={isClientLinux}
/>
{activeTask && (
<TaskPanel
@@ -931,6 +990,20 @@ function MetricCard({ label, value, note, tone }) {
);
}
function ClientOsNotice({ osLabel }) {
return (
<div className="offline-banner client-os-banner">
<AlertCircle size={19} aria-hidden="true" />
<div>
<strong>Web Client chỉ hỗ trợ máy Linux</strong>
<p>
Máy hiện tại được nhận diện {osLabel}. Lệnh cài Agent các thao tác cài đặt đã được ẩn.
</p>
</div>
</div>
);
}
function LocalStatus({ app, task }) {
if (isTaskActive(task)) {
return (
@@ -1053,7 +1126,7 @@ function TaskPanel({ task, onClear, onRefresh }) {
);
}
function AgentPanel({ health, systemInfo, status, latestAgentPackage, needsUpdate, onCopyUpdate }) {
function AgentPanel({ health, systemInfo, status, latestAgentPackage, needsUpdate, onCopyUpdate, showAgentActions }) {
return (
<section className="panel">
<div className="panel-header">
@@ -1094,7 +1167,7 @@ function AgentPanel({ health, systemInfo, status, latestAgentPackage, needsUpdat
<span><Cpu size={14} aria-hidden="true" /> {systemInfo?.kernel || 'kernel -'}</span>
<span><HardDrive size={14} aria-hidden="true" /> {systemInfo?.diskFree || 'disk -'}</span>
</div>
{needsUpdate && (
{needsUpdate && showAgentActions && (
<div className="agent-update-action">
<button className="btn btn-warning" type="button" onClick={onCopyUpdate}>
<Clipboard size={15} aria-hidden="true" />

View File

@@ -545,12 +545,25 @@ code {
border-color: rgba(181, 71, 8, 0.35);
}
.client-os-banner {
background: #fffbeb;
border-color: rgba(181, 71, 8, 0.35);
grid-template-columns: auto minmax(0, 1fr);
}
.offline-banner strong {
color: #111827;
display: block;
font-size: 13px;
}
.client-os-banner p {
color: var(--on-surface-variant);
font-size: 13px;
line-height: 1.45;
margin-top: 4px;
}
.workbench-grid {
display: grid;
flex: 1;