laster 0.0.2

This commit is contained in:
2026-05-26 15:43:56 +07:00
parent e2c4881bb7
commit 8ceb1bb1df
24 changed files with 583 additions and 40 deletions

View File

@@ -37,6 +37,7 @@ import {
fetchTaskComponents,
fetchTaskLogs,
fetchTaskStatus,
getAppOpenUrl,
joinUrl,
normalizeUrl,
queueInstall,
@@ -353,6 +354,34 @@ function App() {
});
}, []);
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 (!agentHealth) {
notify('warning', 'Agent local đang offline. Cài Agent rồi bấm Retry.');
@@ -368,8 +397,14 @@ function App() {
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);
@@ -378,11 +413,17 @@ function App() {
startTask(queuedTask, action, app);
notify('success', `Đã queue task ${queuedTask.taskId}`);
} catch (error) {
notify('failure', getErrorMessage(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, notify, startTask]);
}, [agentBaseUrl, agentHealth, notify, packageBaseUrl, startPreflightFailedTask, startTask]);
const applySettings = useCallback(() => {
const nextSettings = {
@@ -650,6 +691,9 @@ function App() {
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 openUrl = app.installed
? getAppOpenUrl({ ...app, openUrl: app.installed.openUrl || app.openUrl })
: '';
return (
<tr
@@ -700,6 +744,19 @@ function App() {
Update
</button>
)}
{app.installed && openUrl && (
<a
className="btn btn-secondary compact"
href={openUrl}
target="_blank"
rel="noreferrer"
title={`Open ${app.appName}`}
onClick={(event) => event.stopPropagation()}
>
<ExternalLink size={14} aria-hidden="true" />
Open App
</a>
)}
{app.installed && (
<button
className="icon-button danger"
@@ -825,6 +882,7 @@ function TaskPanel({ task, onClear, onRefresh }) {
const logs = task.logs || [];
const visibleLogs = logs.slice(-6);
const canClear = TERMINAL_TASK_STATUSES.has(task.status);
const canRefresh = !task.localOnly;
return (
<section className="panel task-panel">
@@ -835,7 +893,7 @@ function TaskPanel({ task, onClear, onRefresh }) {
</div>
<div className="panel-actions">
<span className={statusBadgeClass(task.status)}>{task.status || 'queued'}</span>
<button className="icon-button subtle" type="button" title="Refresh task" onClick={onRefresh}>
<button className="icon-button subtle" type="button" title="Refresh task" onClick={onRefresh} disabled={!canRefresh}>
<RefreshCcw size={16} aria-hidden="true" />
</button>
{canClear && (
@@ -1001,6 +1059,9 @@ function AppDetailPanel({ app, detail, manifest, status, packageBaseUrl }) {
<ShieldCheck size={15} aria-hidden="true" />
Components
</div>
{status.state === 'danger' && (
<div className="table-empty compact-empty danger-text">{status.message}</div>
)}
{(components.length ? components : packages).slice(0, 5).map((item) => (
<div className="component-item" key={item.componentId || item.id || item.packageId}>
<div>