fix(appstore): резервный REST-опрос прогресса установки (не зависать при потере WS)
All checks were successful
Deploy to Production / Trigger production update (push) Successful in 8s

Мастер установки получал прогресс только по WebSocket; при потерянном
событии (или долгом docker compose up с тяжёлыми образами) UI замирал на
последнем шаге. Добавлен поллинг /operations раз в 3с как фолбэк.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
markmorado
2026-07-08 00:50:31 +05:00
parent 06c4ec8e94
commit e497ae541f
4 changed files with 19 additions and 3 deletions

View File

@@ -1,4 +1,4 @@
import { useCallback, useMemo, useState } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useNavigate } from 'react-router';
import { useTranslation } from 'react-i18next';
import { toast } from 'sonner';
@@ -15,7 +15,7 @@ import {
} from '@/components/ui/select';
import { useServers } from '@/hooks/use-nodes';
import { useIPAMNetworks, useIPAMPools } from '@/hooks/use-ipam';
import { useInstallApp, useInstalledApp, useOperationProgress } from '@/hooks/use-appstore';
import { useInstallApp, useInstalledApp, useOperationProgress, useAppOperations } from '@/hooks/use-appstore';
import type { AppOperation, CatalogApp } from '@/types/appstore';
interface Props {
@@ -62,6 +62,18 @@ export default function InstallWizard({ app, open, onOpenChange }: Props) {
}, []);
useOperationProgress(phase === 'progress' ? installedAppId : null, onOp);
// Резервный опрос статуса через REST: если WebSocket-событие потеряно
// (или установка идёт долго, напр. docker compose тянет тяжёлые образы),
// прогресс всё равно обновляется и мастер не «зависает» на старом шаге.
const { data: polledOps } = useAppOperations(
phase === 'progress' ? (installedAppId ?? 0) : 0,
3000,
);
useEffect(() => {
const latest = polledOps?.[0]; // операции отсортированы по времени (свежая первой)
if (latest && latest.installed_app_id === installedAppId) onOp(latest);
}, [polledOps, installedAppId, onOp]);
// видимые поля формы (random генерируются на сервере, пользователю не показываем)
const visibleFields = useMemo(
() => (app.form_fields ?? []).filter((f) => (f.type || '').toLowerCase() !== 'random'),

View File

@@ -73,11 +73,13 @@ export function useInstalledApp(id: number) {
});
}
export function useAppOperations(id: number) {
export function useAppOperations(id: number, pollMs?: number) {
return useQuery({
queryKey: appstoreKeys.operations(id),
queryFn: () => apiClient.get<{ items: AppOperation[] }>(`/api/appstore/apps/${id}/operations`).then((r) => r.items),
enabled: id > 0,
// Резервный опрос (когда задан) — прогресс не зависит только от WebSocket.
refetchInterval: pollMs && pollMs > 0 ? pollMs : false,
});
}

View File

@@ -39,6 +39,7 @@
"by": "by",
"source": "Source",
"version": "Version",
"port": "Port",
"disclaimer": "This compose file runs as-is from a third-party repository. Review what will be installed.",
"view_compose": "View source compose",
"back": "Back to catalog",

View File

@@ -39,6 +39,7 @@
"by": "автор",
"source": "Источник",
"version": "Версия",
"port": "Порт",
"disclaimer": "Этот compose-файл исполняется как есть из стороннего репозитория. Проверьте, что будет установлено.",
"view_compose": "Показать исходный compose",
"back": "К каталогу",