feat: админ-панель, лицензия, тестовый стек на распределённой инфраструктуре

Админ-панель (доступ: is_admin + секретный код сессии в /admin/<code>):
- разделы: дашборд (здоровье сервисов, статистика), клиенты, работы,
  база документов, хранилище MinIO, источники парсинга, отстойник работ
- backend: модели ParseSource/StagedWork/AdminSession, миграция 003,
  core/admin (авторизация), роутер api/admin
- frontend: AdminLayout + 7 вкладок, adminApi, вход из кабинета
- Celery index.run_parser (фоновый парсинг), автосбор проверенных работ
  в отстойник с ручным одобрением → индексация в базу

Исправления:
- openalex parser: тип article (не journal-article), убран is_oa-фильтр
- winnowing: приведение xxh64 к signed int64 (фикс bigint out of range)
- bcrypt пин 4.0.1 (совместимость с passlib 1.7.4)
- alembic: prepend_sys_path + asyncpg-драйвер
- frontend: контракт Task (public_id вместо id), react-dropzone

Инфраструктура:
- docker-compose.test.yml: api+frontend+воркеры локально, БД/кэш/хранилище/
  брокер/LLM — на удалённых серверах через .env
- .gitignore: исправлено ошибочное игнорирование кода services/*/app/models/
- LICENSE: проприетарная

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
jze9
2026-05-30 20:45:53 +05:00
parent 9ee5ccdc3f
commit 9894fa9320
50 changed files with 5680 additions and 58 deletions

View File

@@ -1,9 +1,10 @@
import React from 'react';
import { Navigate } from 'react-router-dom';
import { Navigate, useNavigate } from 'react-router-dom';
import { useQuery } from '@tanstack/react-query';
import { Crown, Search, Shield, BookOpen } from 'lucide-react';
import { Crown, Search, Shield, BookOpen, ShieldAlert } from 'lucide-react';
import toast from 'react-hot-toast';
import { TaskCard } from '../components/TaskCard';
import { tasksApi } from '../api/client';
import { tasksApi, adminApi } from '../api/client';
import { useAuthStore } from '../store/auth';
import { PLAN_LIMITS, type Task } from '../types';
@@ -16,6 +17,16 @@ const PLAN_BADGE_STYLES = {
export function Cabinet() {
const { isAuthenticated, user } = useAuthStore();
const navigate = useNavigate();
const openAdmin = async () => {
try {
const { data } = await adminApi.openSession();
navigate(`${data.url}/dashboard`);
} catch {
toast.error('Не удалось открыть админ-панель');
}
};
if (!isAuthenticated) {
return <Navigate to="/login" replace />;
@@ -49,9 +60,20 @@ export function Cabinet() {
</div>
<p className="text-gray-500 text-sm">{user?.email}</p>
</div>
<button className="px-4 py-2 text-sm font-medium bg-brand-600 text-white rounded-lg hover:bg-brand-700 transition-colors">
Обновить план
</button>
<div className="flex items-center gap-2">
{user?.is_admin && (
<button
onClick={openAdmin}
className="flex items-center gap-1.5 px-4 py-2 text-sm font-medium bg-gray-900 text-white rounded-lg hover:bg-gray-800 transition-colors"
>
<ShieldAlert className="w-4 h-4" />
Админ-панель
</button>
)}
<button className="px-4 py-2 text-sm font-medium bg-brand-600 text-white rounded-lg hover:bg-brand-700 transition-colors">
Обновить план
</button>
</div>
</div>
</div>
@@ -116,7 +138,7 @@ export function Cabinet() {
{tasks && tasks.length > 0 && (
<div className="space-y-2">
{tasks.map((task) => (
<TaskCard key={task.id} task={task} />
<TaskCard key={task.public_id} task={task} />
))}
</div>
)}