feat(infra): переход прод/test на общую инфраструктуру вместо self-hosted

Postgres/Redis/RabbitMQ/MinIO/Ollama теперь общие серверы сети (адреса в .env),
локально в докере остаётся только Elasticsearch + app-сервисы. Старый
docker-compose.yml (полностью автономный стек) сохранён как
docker-compose.selfhosted.yml.example на случай отдельного GPU-сервера в
будущем. Добавлен docker-compose.prod.yml (app + свой nginx с TLS,
собирающий фронтенд в статику). Makefile переписан под три режима
(dev/test-up/prod). Мелкая чистка неиспользуемых импортов во фронтенде.
This commit is contained in:
jze9
2026-07-27 12:22:36 +05:00
parent 25e55a3f7d
commit 673e72a15a
26 changed files with 245 additions and 76 deletions

View File

@@ -1,11 +1,18 @@
.PHONY: dev build migrate logs shell-api shell-gpu lint test down ps restart clean .PHONY: dev build migrate logs shell-api shell-gpu lint test down ps restart clean test-up test-down test-logs test-ps
# ─── Переменные ──────────────────────────────────────────────────────────────── # ─── Переменные ────────────────────────────────────────────────────────────────
COMPOSE = docker compose # Прод: docker-compose.prod.yml — app-сервисы + локальный ES, остальная инфра
COMPOSE_DEV = docker compose -f docker-compose.yml -f docker-compose.dev.yml # (postgres/redis/rabbitmq/minio/ollama) уже существует и берётся из .env.
# Dev (полностью локальный sandbox, свои postgres/redis/rabbitmq/minio/ollama +
# hot reload) — поверх self-hosted примера, используется редко.
# Test (то, что реально гоняется на машине разработчика день в день) — hot
# reload против той же общей инфры, что и прод.
COMPOSE_PROD = docker compose -f docker-compose.prod.yml
COMPOSE_DEV = docker compose -f docker-compose.selfhosted.yml.example -f docker-compose.dev.yml
COMPOSE_TEST = docker compose -f docker-compose.test.yml
PROJECT = anti-plagiarism PROJECT = anti-plagiarism
# ─── Разработка ──────────────────────────────────────────────────────────────── # ─── Разработка (полностью локальный sandbox, редко используется) ─────────────
dev: dev:
@cp -n .env.example .env 2>/dev/null || true @cp -n .env.example .env 2>/dev/null || true
$(COMPOSE_DEV) up --build $(COMPOSE_DEV) up --build
@@ -14,99 +21,112 @@ dev-d:
@cp -n .env.example .env 2>/dev/null || true @cp -n .env.example .env 2>/dev/null || true
$(COMPOSE_DEV) up --build -d $(COMPOSE_DEV) up --build -d
# ─── Test (hot reload против общей инфры — повседневная разработка) ──────────
test-up:
$(COMPOSE_TEST) up --build -d
test-down:
$(COMPOSE_TEST) down
test-logs:
$(COMPOSE_TEST) logs -f
test-ps:
$(COMPOSE_TEST) ps
# ─── Продакшн ───────────────────────────────────────────────────────────────── # ─── Продакшн ─────────────────────────────────────────────────────────────────
build: build:
$(COMPOSE) build $(COMPOSE_PROD) build
up: up:
$(COMPOSE) up -d $(COMPOSE_PROD) up -d
down: down:
$(COMPOSE) down $(COMPOSE_PROD) down
restart: restart:
$(COMPOSE) restart $(COMPOSE_PROD) restart
ps: ps:
$(COMPOSE) ps $(COMPOSE_PROD) ps
# ─── Миграции ───────────────────────────────────────────────────────────────── # ─── Миграции ─────────────────────────────────────────────────────────────────
migrate: migrate:
$(COMPOSE) exec api alembic upgrade head $(COMPOSE_PROD) exec api alembic upgrade head
migrate-dev: migrate-dev:
$(COMPOSE_DEV) exec api alembic upgrade head $(COMPOSE_DEV) exec api alembic upgrade head
makemigration: makemigration:
@read -p "Migration name: " name; \ @read -p "Migration name: " name; \
$(COMPOSE) exec api alembic revision --autogenerate -m "$$name" $(COMPOSE_PROD) exec api alembic revision --autogenerate -m "$$name"
downgrade: downgrade:
$(COMPOSE) exec api alembic downgrade -1 $(COMPOSE_PROD) exec api alembic downgrade -1
# ─── Логи ───────────────────────────────────────────────────────────────────── # ─── Логи ─────────────────────────────────────────────────────────────────────
logs: logs:
$(COMPOSE) logs -f $(COMPOSE_PROD) logs -f
logs-api: logs-api:
$(COMPOSE) logs -f api $(COMPOSE_PROD) logs -f api
logs-gpu: logs-gpu:
$(COMPOSE) logs -f worker-gpu $(COMPOSE_PROD) logs -f worker-gpu
logs-indexer: logs-indexer:
$(COMPOSE) logs -f worker-indexer $(COMPOSE_PROD) logs -f worker-indexer
logs-notifier: logs-notifier:
$(COMPOSE) logs -f worker-notifier $(COMPOSE_PROD) logs -f worker-notifier
logs-gost: logs-gost:
$(COMPOSE) logs -f worker-gost $(COMPOSE_PROD) logs -f worker-gost
# ─── Шеллы ──────────────────────────────────────────────────────────────────── # ─── Шеллы ────────────────────────────────────────────────────────────────────
shell-api: shell-api:
$(COMPOSE) exec api bash $(COMPOSE_PROD) exec api bash
shell-gpu: shell-gpu:
$(COMPOSE) exec worker-gpu bash $(COMPOSE_PROD) exec worker-gpu bash
shell-indexer: shell-indexer:
$(COMPOSE) exec worker-indexer bash $(COMPOSE_PROD) exec worker-indexer bash
shell-db: shell-db:
$(COMPOSE) exec postgres psql -U antiplagiator antiplagiator @echo "Postgres общий (192.168.1.11) — подключайся напрямую: psql -h 192.168.1.11 -U antiplagiator antiplagiator"
shell-redis: shell-redis:
$(COMPOSE) exec redis redis-cli @echo "Redis общий (192.168.1.19) — подключайся напрямую: redis-cli -h 192.168.1.19 -n 3"
# ─── Линтинг и тесты ────────────────────────────────────────────────────────── # ─── Линтинг и тесты ──────────────────────────────────────────────────────────
lint: lint:
$(COMPOSE) exec api ruff check . --fix $(COMPOSE_PROD) exec api ruff check . --fix
$(COMPOSE) exec api mypy app/ $(COMPOSE_PROD) exec api mypy app/
lint-frontend: lint-frontend:
cd services/frontend && npm run lint cd services/frontend && npm run lint
test: test:
$(COMPOSE) exec api pytest tests/ -v $(COMPOSE_PROD) exec api pytest tests/ -v
test-cov: test-cov:
$(COMPOSE) exec api pytest tests/ -v --cov=app --cov-report=html $(COMPOSE_PROD) exec api pytest tests/ -v --cov=app --cov-report=html
# ─── Утилиты ────────────────────────────────────────────────────────────────── # ─── Утилиты ──────────────────────────────────────────────────────────────────
# ВНИМАНИЕ: -v удаляет только ЛОКАЛЬНЫЕ volume (es_prod_data, faiss_index_prod) —
# общие postgres/redis/rabbitmq/minio не в Compose, их не затронет.
clean: clean:
$(COMPOSE) down -v --remove-orphans $(COMPOSE_PROD) down -v --remove-orphans
docker system prune -f docker system prune -f
# Запустить парсер (пример: make parse PARSER=openalex ARGS="--limit 1000") # Запустить парсер (пример: make parse PARSER=openalex ARGS="--limit 1000")
parse: parse:
$(COMPOSE) exec api python /scripts/run_parser.py $(PARSER) $(ARGS) $(COMPOSE_PROD) exec api python /scripts/run_parser.py $(PARSER) $(ARGS)
# Создать MinIO бакеты # Создать MinIO бакеты (запускать один раз на общем MinIO 192.168.1.21, не локально)
minio-init: minio-init:
$(COMPOSE) exec minio mc alias set local http://localhost:9000 minioadmin changeme @echo "MinIO общий (192.168.1.21) — бакеты documents/backups создаются на нём, см. .env MINIO_*"
$(COMPOSE) exec minio mc mb local/documents || true
$(COMPOSE) exec minio mc mb local/backups || true
# Создать ES индекс # Создать ES индекс
es-init: es-init:
@@ -115,13 +135,14 @@ es-init:
# Статистика Celery # Статистика Celery
flower: flower:
@echo "Flower доступен по адресу http://localhost:5555" @echo "Flower доступен по адресу http://localhost:5555"
@$(COMPOSE) ps flower @$(COMPOSE_PROD) ps flower
help: help:
@echo "Академический помощник — команды make:" @echo "Академический помощник — команды make:"
@echo "" @echo ""
@echo " make dev — запустить в режиме разработки (hot reload)" @echo " make test-up — запустить тестовый стек (hot reload, общая инфра)"
@echo " make buildсобрать Docker образы" @echo " make dev запустить полностью локальный sandbox (редко нужно)"
@echo " make build — собрать прод-образы"
@echo " make up — запустить в продакшн режиме" @echo " make up — запустить в продакшн режиме"
@echo " make down — остановить все сервисы" @echo " make down — остановить все сервисы"
@echo " make migrate — применить миграции Alembic" @echo " make migrate — применить миграции Alembic"
@@ -129,4 +150,4 @@ help:
@echo " make shell-api — открыть shell в контейнере api" @echo " make shell-api — открыть shell в контейнере api"
@echo " make lint — запустить линтер" @echo " make lint — запустить линтер"
@echo " make test — запустить тесты" @echo " make test — запустить тесты"
@echo " make clean — удалить все контейнеры и volumes" @echo " make clean — удалить прод-контейнеры и ЛОКАЛЬНЫЕ volume"

View File

@@ -134,21 +134,31 @@ python scripts/run_parser.py arxiv \
Смотри `.env.example` для полного списка переменных. Смотри `.env.example` для полного списка переменных.
Обязательно смените `SECRET_KEY`, `POSTGRES_PASSWORD`, `MINIO_SECRET_KEY`. Обязательно смените `SECRET_KEY`, `POSTGRES_PASSWORD`, `MINIO_SECRET_KEY`.
## Три docker-compose файла
| Файл | Назначение |
|------|-----------|
| `docker-compose.prod.yml` | **Реальный прод.** app-сервисы + nginx (собранный фронтенд + TLS) + локальный Elasticsearch. Postgres/Redis/RabbitMQ/MinIO/Ollama — уже существующие общие серверы, адреса в `.env`. |
| `docker-compose.test.yml` | Повседневная разработка — hot reload против той же общей инфры, что и прод (`make test-up`). |
| `docker-compose.selfhosted.yml.example` | Не используется. Полностью автономный вариант (свои Postgres/Redis/RabbitMQ/ES/MinIO/Ollama + GPU passthrough) — на случай отдельного выделенного сервера в будущем. |
## Продакшн деплой ## Продакшн деплой
```bash ```bash
# На сервере # На сервере
cp .env.example .env cp .env.example .env
nano .env # Настроить все пароли и ключи nano .env # Настроить все пароли и ключи, указать реальные адреса общих Postgres/Redis/RabbitMQ/MinIO/Ollama
# Сертификат ДО первого запуска nginx-контейнера (standalone, порт 80 должен быть свободен)
certbot certonly --standalone -d academic.jze9.ru
make build make build
make up make up
make migrate make migrate
# SSL сертификат (Let's Encrypt)
certbot --nginx -d academic.jze9.ru
``` ```
Nginx работает внутри контейнера (`docker-compose.prod.yml`, сервис `nginx`, Dockerfile в `infra/nginx/Dockerfile`) — собирает `services/frontend` в статику и отдаёт её вместе с проксированием `/api/`, `/ws/` на `api:8000` по конфигу `infra/nginx/nginx.conf`. Контейнер монтирует `/etc/letsencrypt` с хоста как read-only — сертификат обновляется на хосте (`certbot renew`), контейнер просто читает его.
## Лицензия ## Лицензия
MIT MIT

129
docker-compose.prod.yml Normal file
View File

@@ -0,0 +1,129 @@
# ═══════════════════════════════════════════════════════════════════════════════
# ПРОД стек: локально — nginx (static+proxy), api, воркеры, elasticsearch.
# Удалённо (через .env) — postgres, redis, minio, ollama, rabbitmq.
# Тот же принцип, что и docker-compose.test.yml (не дублировать общую инфру),
# но без bind-mount'ов исходников, без --reload, с собранным фронтендом и nginx/TLS.
# Запуск: docker compose -f docker-compose.prod.yml up -d --build
# ═══════════════════════════════════════════════════════════════════════════════
networks:
antiplagiator:
driver: bridge
volumes:
es_prod_data:
faiss_index_prod:
x-app-env: &app-env
env_file: .env
networks:
- antiplagiator
restart: unless-stopped
services:
# ─── Локальная инфраструктура ──────────────────────────────────────────────
elasticsearch:
image: elasticsearch:8.13.0
container_name: antiplagiator-elasticsearch
environment:
- discovery.type=single-node
- xpack.security.enabled=false
- xpack.ml.enabled=false
- ES_JAVA_OPTS=-Xms2g -Xmx2g
- bootstrap.memory_lock=true
- cluster.name=antiplagiator-prod
ulimits:
memlock:
soft: -1
hard: -1
volumes:
- es_prod_data:/usr/share/elasticsearch/data
networks:
- antiplagiator
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "curl -s http://localhost:9200/_cluster/health | grep -qv '\"status\":\"red\"'"]
interval: 15s
timeout: 10s
retries: 10
start_period: 60s
# ─── Приложение ────────────────────────────────────────────────────────────
# RabbitMQ вынесен на отдельный сервер 192.168.20.82 (см. RABBITMQ_URL в .env)
api:
build:
context: ./services/api
dockerfile: Dockerfile
container_name: antiplagiator-api
<<: *app-env
depends_on:
elasticsearch:
condition: service_healthy
worker-gpu:
build:
context: ./services/worker-gpu
dockerfile: Dockerfile
container_name: antiplagiator-worker-gpu
<<: *app-env
volumes:
- faiss_index_prod:/data/index
depends_on:
elasticsearch:
condition: service_healthy
worker-indexer:
build:
context: ./services/worker-indexer
dockerfile: Dockerfile
container_name: antiplagiator-worker-indexer
<<: *app-env
volumes:
- ./scripts/parsers:/parsers:ro
depends_on:
elasticsearch:
condition: service_healthy
worker-notifier:
build:
context: ./services/worker-notifier
dockerfile: Dockerfile
container_name: antiplagiator-worker-notifier
<<: *app-env
worker-gost:
build:
context: ./services/worker-gost
dockerfile: Dockerfile
container_name: antiplagiator-worker-gost
<<: *app-env
flower:
image: mher/flower:2.0
container_name: antiplagiator-flower
command: celery --broker=${RABBITMQ_URL} flower --port=5555
environment:
CELERY_BROKER_URL: ${RABBITMQ_URL}
CELERY_RESULT_BACKEND: ${REDIS_URL}
networks:
- antiplagiator
restart: unless-stopped
# Flower не публикуется наружу напрямую — доступ через ssh-туннель при необходимости
# ─── Nginx: собранный фронтенд (static) + proxy /api, /ws → api:8000, TLS ──
nginx:
build:
context: .
dockerfile: infra/nginx/Dockerfile
container_name: antiplagiator-nginx
networks:
- antiplagiator
ports:
- "80:80"
- "443:443"
volumes:
- /etc/letsencrypt:/etc/letsencrypt:ro
depends_on:
- api
restart: unless-stopped

View File

@@ -1,5 +1,13 @@
# ═══════════════════════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════════════════════
# Академический помощник — Production Docker Compose # Академический помощник — self-hosted вариант (свои Postgres/Redis/RabbitMQ/
# ES/MinIO/Ollama + GPU passthrough для ollama/worker-gpu).
#
# НЕ используется сейчас — реальный прод разворачивается через
# docker-compose.prod.yml поверх уже существующей общей инфры (Postgres .11,
# Redis .19, RabbitMQ .82, MinIO .21, Ollama CT108). Держим этот файл на случай,
# если когда-нибудь появится отдельный GPU-сервер под этот проект — тогда
# у worker-indexer нужно будет ещё добавить volume ./scripts/parsers:/parsers:ro
# (в этом файле его сейчас нет, в отличие от prod/test).
# ═══════════════════════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════════════════════
networks: networks:
@@ -13,7 +21,7 @@ volumes:
elasticsearch_data: elasticsearch_data:
minio_data: minio_data:
ollama_data: ollama_data:
faiss_index: faiss_index_selfhosted:
# ─── Общие переменные окружения для app-сервисов ──────────────────────────── # ─── Общие переменные окружения для app-сервисов ────────────────────────────
x-app-env: &app-env x-app-env: &app-env
@@ -177,7 +185,7 @@ services:
<<: *app-env <<: *app-env
command: celery -A app.celery_app worker -Q queue.gpu -c 1 -n gpu@%h --loglevel=info command: celery -A app.celery_app worker -Q queue.gpu -c 1 -n gpu@%h --loglevel=info
volumes: volumes:
- faiss_index:/data/index - faiss_index_selfhosted:/data/index
depends_on: depends_on:
postgres: postgres:
condition: service_healthy condition: service_healthy

10
infra/nginx/Dockerfile Normal file
View File

@@ -0,0 +1,10 @@
FROM node:20-slim AS build
WORKDIR /app
COPY services/frontend/package.json services/frontend/package-lock.json* ./
RUN npm install
COPY services/frontend/ .
RUN npm run build
FROM nginx:1.27-alpine
COPY --from=build /app/dist /var/www/academic
COPY infra/nginx/nginx.conf /etc/nginx/nginx.conf

View File

@@ -72,6 +72,12 @@ http {
add_header X-Frame-Options DENY; add_header X-Frame-Options DENY;
add_header X-XSS-Protection "1; mode=block"; add_header X-XSS-Protection "1; mode=block";
# ── Health check (не под /api/ в самом приложении) ─────────────────────
location = /health {
proxy_pass http://api/health;
proxy_set_header Host $host;
}
# ── API proxy ────────────────────────────────────────────────────────── # ── API proxy ──────────────────────────────────────────────────────────
location /api/ { location /api/ {
proxy_pass http://api/api/; proxy_pass http://api/api/;

View File

@@ -1,4 +1,4 @@
import React, { useCallback } from 'react'; import { useCallback } from 'react';
import { useDropzone } from 'react-dropzone'; import { useDropzone } from 'react-dropzone';
import { Upload, FileText, X } from 'lucide-react'; import { Upload, FileText, X } from 'lucide-react';
import { clsx } from 'clsx'; import { clsx } from 'clsx';

View File

@@ -1,4 +1,3 @@
import React from 'react';
import { Copy, Check } from 'lucide-react'; import { Copy, Check } from 'lucide-react';
import { useState } from 'react'; import { useState } from 'react';

View File

@@ -1,6 +1,5 @@
import React from 'react';
import { Link, NavLink, useNavigate } from 'react-router-dom'; import { Link, NavLink, useNavigate } from 'react-router-dom';
import { GraduationCap, Search, BookOpen, Upload, LayoutDashboard, LogOut, User, BookMarked } from 'lucide-react'; import { GraduationCap, Search, BookOpen, Upload, LayoutDashboard, LogOut } from 'lucide-react';
import { clsx } from 'clsx'; import { clsx } from 'clsx';
import { useAuthStore } from '../store/auth'; import { useAuthStore } from '../store/auth';
import { EmailVerificationModal } from './EmailVerificationModal'; import { EmailVerificationModal } from './EmailVerificationModal';

View File

@@ -1,4 +1,3 @@
import React from 'react';
import { AlertTriangle, CheckCircle, Info } from 'lucide-react'; import { AlertTriangle, CheckCircle, Info } from 'lucide-react';
import { clsx } from 'clsx'; import { clsx } from 'clsx';
import type { PlagiarismResultData } from '../types'; import type { PlagiarismResultData } from '../types';

View File

@@ -1,4 +1,3 @@
import React from 'react';
import { ExternalLink, BookmarkPlus, BookmarkCheck, Copy } from 'lucide-react'; import { ExternalLink, BookmarkPlus, BookmarkCheck, Copy } from 'lucide-react';
import { clsx } from 'clsx'; import { clsx } from 'clsx';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';

View File

@@ -1,4 +1,3 @@
import React from 'react';
import { clsx } from 'clsx'; import { clsx } from 'clsx';
import type { TaskStatus } from '../types'; import type { TaskStatus } from '../types';

View File

@@ -1,4 +1,3 @@
import React from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { formatDistanceToNow } from 'date-fns'; import { formatDistanceToNow } from 'date-fns';
import { ru } from 'date-fns/locale'; import { ru } from 'date-fns/locale';

View File

@@ -1,16 +1,14 @@
import React, { useState } from 'react'; import { useState } from 'react';
import { useMutation } from '@tanstack/react-query'; import { useMutation } from '@tanstack/react-query';
import { BookOpen, Copy, Trash2, BookMarked } from 'lucide-react'; import { BookOpen, Copy, Trash2, BookMarked } from 'lucide-react';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
import { GostCitation } from '../components/GostCitation'; import { GostCitation } from '../components/GostCitation';
import { useBibliographyStore } from '../store/bibliography'; import { useBibliographyStore } from '../store/bibliography';
import { useAuthStore } from '../store/auth';
import { api } from '../api/client'; import { api } from '../api/client';
import type { GostResultData } from '../types'; import type { GostResultData } from '../types';
export function Bibliography() { export function Bibliography() {
const { sources, removeSource, clearSources } = useBibliographyStore(); const { sources, removeSource, clearSources } = useBibliographyStore();
const { isAuthenticated } = useAuthStore();
const [style, setStyle] = useState<'7.1' | '7.0.5'>('7.1'); const [style, setStyle] = useState<'7.1' | '7.0.5'>('7.1');
const [formattedResult, setFormattedResult] = useState<GostResultData | null>(null); const [formattedResult, setFormattedResult] = useState<GostResultData | null>(null);

View File

@@ -1,4 +1,3 @@
import React from 'react';
import { Navigate, useNavigate } from 'react-router-dom'; import { Navigate, useNavigate } from 'react-router-dom';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { Crown, Search, Shield, BookOpen, ShieldAlert } from 'lucide-react'; import { Crown, Search, Shield, BookOpen, ShieldAlert } from 'lucide-react';

View File

@@ -1,4 +1,3 @@
import React from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { Search, Shield, BookOpen, Zap } from 'lucide-react'; import { Search, Shield, BookOpen, Zap } from 'lucide-react';
import { SearchBar } from '../components/SearchBar'; import { SearchBar } from '../components/SearchBar';

View File

@@ -1,4 +1,3 @@
import React from 'react';
import { Check } from 'lucide-react'; import { Check } from 'lucide-react';
import { clsx } from 'clsx'; import { clsx } from 'clsx';
import { PLAN_LIMITS } from '../types'; import { PLAN_LIMITS } from '../types';

View File

@@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom'; import { useNavigate, useSearchParams } from 'react-router-dom';
import { useQuery, useMutation } from '@tanstack/react-query'; import { useQuery, useMutation } from '@tanstack/react-query';
import { Filter, Loader2 } from 'lucide-react'; import { Filter, Loader2 } from 'lucide-react';
@@ -41,16 +41,13 @@ export function Search() {
}); });
// Поллинг задачи // Поллинг задачи
const { const { data: task } = useQuery({
data: task,
isLoading: isPolling,
} = useQuery({
queryKey: ['task', taskId], queryKey: ['task', taskId],
queryFn: () => tasksApi.get(taskId!).then((r) => r.data as Task), queryFn: () => tasksApi.get(taskId!).then((r) => r.data as Task),
enabled: !!taskId, enabled: !!taskId,
refetchInterval: (data) => { refetchInterval: (query) => {
if (!data) return 3000; const status = query.state.data?.status;
if (data.status === 'done' || data.status === 'failed') return false; if (status === 'done' || status === 'failed') return false;
return 3000; return 3000;
}, },
}); });

View File

@@ -1,4 +1,3 @@
import React from 'react';
import { useParams, Navigate } from 'react-router-dom'; import { useParams, Navigate } from 'react-router-dom';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { Loader2, ArrowLeft } from 'lucide-react'; import { Loader2, ArrowLeft } from 'lucide-react';
@@ -22,9 +21,9 @@ export function Task() {
const { data: task, isLoading } = useQuery({ const { data: task, isLoading } = useQuery({
queryKey: ['task', taskId], queryKey: ['task', taskId],
queryFn: () => tasksApi.get(taskId).then((r) => r.data as TaskType), queryFn: () => tasksApi.get(taskId).then((r) => r.data as TaskType),
refetchInterval: (data) => { refetchInterval: (query) => {
if (!data) return 3000; const status = query.state.data?.status;
return (data.status === 'done' || data.status === 'failed') ? false : 3000; return (status === 'done' || status === 'failed') ? false : 3000;
}, },
}); });

View File

@@ -1,4 +1,4 @@
import React, { useState } from 'react'; import { useState } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { Trash2, Search } from 'lucide-react'; import { Trash2, Search } from 'lucide-react';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';

View File

@@ -1,4 +1,4 @@
import React, { useState } from 'react'; import { useState } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { Play, Trash2, Plus } from 'lucide-react'; import { Play, Trash2, Plus } from 'lucide-react';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';

View File

@@ -1,4 +1,4 @@
import React, { useState } from 'react'; import { useState } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { Check, X, Eye } from 'lucide-react'; import { Check, X, Eye } from 'lucide-react';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';

View File

@@ -1,4 +1,3 @@
import React from 'react';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { HardDrive } from 'lucide-react'; import { HardDrive } from 'lucide-react';
import { adminApi } from '../../api/client'; import { adminApi } from '../../api/client';

View File

@@ -1,4 +1,4 @@
import React, { useState } from 'react'; import { useState } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { Trash2, Search } from 'lucide-react'; import { Trash2, Search } from 'lucide-react';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';

View File

@@ -1,4 +1,4 @@
import React, { useState } from 'react'; import { useState } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { RefreshCw, Trash2 } from 'lucide-react'; import { RefreshCw, Trash2 } from 'lucide-react';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';

1
services/frontend/src/vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
/// <reference types="vite/client" />