Заливка корпуса была чёрным ящиком: у источника только 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>
146 lines
5.4 KiB
TypeScript
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`),
|
|
};
|