Files
anti-plagiarism/services/frontend/src/api/client.ts
jze9 78e27806b9
All checks were successful
Deploy / test (push) Successful in 3m56s
Deploy / deploy (push) Successful in 31s
feat(admin): шкала загрузки источников, отладка и загрузка работ в корпус
Заливка корпуса была чёрным ящиком: у источника только last_status
(idle/running/done/error), без «сколько из скольки», без причины падения и без
способа остановить начатое. Теперь каждый запуск создаёт строку parse_runs,
куда воркер раз в ~2с пишет стадию, счётчики и журнал событий.

Админка:
- шкала загрузки у каждого источника (0→50% выборка, 50→100% индексация),
  раскрытая строка — журнал прогона по шагам с таймингами;
- «Запустить всё» / «Остановить всё» и остановка по одному источнику
  (кооперативная отмена: воркер останавливается сам, не рвя запись в базу);
- пакетное добавление источников (тип + список тем), тип pmc в форме;
- загрузка PDF/DOCX/TXT прямо в базу сравнения (index.ingest_upload);
- страница «Отладка»: воркеры Celery и их текущие таски, очереди RabbitMQ,
  покрытие корпуса эмбеддингами, зависшие и упавшие прогоны, конфиг бэкендов.

Защита от краш-лупа по consumer_timeout RabbitMQ (docs/DR-HA.md §6), без неё
массовый запуск 170+ источников гарантированно ронял воркер:
- PARSER_TIME_BUDGET_S (1500с) — прогон закругляется сам и помечается partial;
- worker_prefetch_multiplier=1 — таймаут считается от ДОСТАВКИ сообщения, и с
  дефолтным префетчем очередь долгих run_parser убивала канал на задачах,
  которые ещё не начинались.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 17:40:23 +05:00

146 lines
5.4 KiB
TypeScript

import axios from 'axios';
import { useAuthStore } from '../store/auth';
export const api = axios.create({
baseURL: import.meta.env.VITE_API_URL || '/api',
headers: { 'Content-Type': 'application/json' },
timeout: 30000,
});
// Добавить JWT токен к каждому запросу
api.interceptors.request.use((config) => {
const token = useAuthStore.getState().token;
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
// Обработка ответов: при 401 — разлогинить
api.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401) {
useAuthStore.getState().logout();
}
return Promise.reject(error);
}
);
// ─── API методы ───────────────────────────────────────────────────────────────
export const authApi = {
register: (data: { email: string; password: string; name: string }) =>
api.post('/auth/register', data),
login: (data: { email: string; password: string }) =>
api.post('/auth/login', data),
me: () => api.get('/auth/me'),
updateProfile: (data: { name?: string; email?: string }) =>
api.patch('/auth/me', data),
changePassword: (data: { current_password: string; new_password: string }) =>
api.post('/auth/change-password', data),
verifyEmail: (token: string) =>
api.post(`/auth/verify-email/${token}`),
resendVerification: () =>
api.post('/auth/resend-verification'),
};
export const tasksApi = {
list: (limit = 20, offset = 0) =>
api.get('/tasks/', { params: { limit, offset } }),
get: (taskId: string) =>
api.get(`/tasks/${taskId}`),
getText: (taskId: string) =>
api.get(`/tasks/${taskId}/text`),
delete: (taskId: string) =>
api.delete(`/tasks/${taskId}`),
};
export const searchApi = {
create: (data: {
query: string;
lang?: string;
year_from?: number;
year_to?: number;
category?: string;
}) => api.post('/search/', data),
};
export const documentsApi = {
uploadForCheck: (file: File) => {
const formData = new FormData();
formData.append('file', file);
return api.post('/documents/check', formData, {
headers: { 'Content-Type': 'multipart/form-data' },
timeout: 120000, // 2 минуты для загрузки
});
},
};
// ─── Админка ────────────────────────────────────────────────────────────────
export const adminApi = {
openSession: () => api.post('/admin/session'),
verifySession: (code: string) => api.get(`/admin/session/verify/${code}`),
health: () => api.get('/admin/health'),
stats: () => api.get('/admin/stats'),
users: (params?: { q?: string; limit?: number; offset?: number }) =>
api.get('/admin/users', { params }),
updateUser: (id: number, data: { plan?: string; is_verified?: boolean; is_admin?: boolean }) =>
api.patch(`/admin/users/${id}`, data),
deleteUser: (id: number) => api.delete(`/admin/users/${id}`),
tasks: (params?: { status?: string; type?: string; user_id?: number; limit?: number; offset?: number }) =>
api.get('/admin/tasks', { params }),
task: (publicId: string) => api.get(`/admin/tasks/${publicId}`),
requeueTask: (publicId: string) => api.post(`/admin/tasks/${publicId}/requeue`),
deleteTask: (publicId: string) => api.delete(`/admin/tasks/${publicId}`),
documents: (params?: { q?: string; source?: string; year?: number; limit?: number; offset?: number }) =>
api.get('/admin/documents', { params }),
deleteDocument: (id: number) => api.delete(`/admin/documents/${id}`),
storage: () => api.get('/admin/storage'),
sources: () => api.get('/admin/sources'),
createSource: (data: Record<string, unknown>) => api.post('/admin/sources', data),
createSourcesBulk: (data: Record<string, unknown>) => api.post('/admin/sources/bulk', data),
updateSource: (id: number, data: Record<string, unknown>) => api.patch(`/admin/sources/${id}`, data),
deleteSource: (id: number) => api.delete(`/admin/sources/${id}`),
runSource: (id: number) => api.post(`/admin/sources/${id}/run`),
cancelSource: (id: number) => api.post(`/admin/sources/${id}/cancel`),
runAllSources: (sourceType?: string) =>
api.post('/admin/sources/run-all', null, { params: sourceType ? { source_type: sourceType } : undefined }),
stopAllSources: () => api.post('/admin/sources/stop-all'),
sourceRuns: (id: number) => api.get(`/admin/sources/${id}/runs`),
activeRuns: () => api.get('/admin/runs/active'),
run: (runId: number) => api.get(`/admin/runs/${runId}`),
debug: () => api.get('/admin/debug'),
uploadDocuments: (files: File[]) => {
const form = new FormData();
files.forEach((f) => form.append('files', f));
return api.post('/admin/documents/upload', form, {
headers: { 'Content-Type': 'multipart/form-data' },
timeout: 300000, // пачка файлов грузится дольше одиночной проверки
});
},
staging: (params?: { status?: string; limit?: number; offset?: number }) =>
api.get('/admin/staging', { params }),
stagingDetail: (id: number) => api.get(`/admin/staging/${id}`),
approveStaging: (id: number) => api.post(`/admin/staging/${id}/approve`),
rejectStaging: (id: number) => api.post(`/admin/staging/${id}/reject`),
};