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

View File

@@ -545,12 +545,25 @@ code {
border-color: rgba(181, 71, 8, 0.35); 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 { .offline-banner strong {
color: #111827; color: #111827;
display: block; display: block;
font-size: 13px; font-size: 13px;
} }
.client-os-banner p {
color: var(--on-surface-variant);
font-size: 13px;
line-height: 1.45;
margin-top: 4px;
}
.workbench-grid { .workbench-grid {
display: grid; display: grid;
flex: 1; flex: 1;