Files
vector/vector_tasks.md
T

639 lines
41 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# ВЕКТОР — очередь технического долга для делегирования
**Проект:** ВЕКТОР (Система определения приоритетных направлений поиска, SAR/МЧС)
**Стек:** FastAPI + PostgreSQL 16 + React (CRA), docker-compose
**Расположение:** LXC 108 (`Lab`, 192.168.0.108), путь `/root/vector`
**Дата анализа:** 2026-07-21
**Состояние на момент анализа:** git чистый, ветка `master`, `126 passed, 26 skipped` (async-тесты пропущены — нет `pytest-asyncio`).
## Как пользоваться этим файлом
- **Раздел A — ATOMIC.** Задачи для локальной LLM (Qwen2.5-coder, 12GB VRAM, ~16–32k контекста). Каждый промт самодостаточен: весь нужный код встроен в промт, модель НЕ читает остальной проект. Бери задачу, копируй блок `PROMPT` целиком в модель, применяй ответ, проверяй по `Критерий приёмки`.
- **Раздел B — NEEDS-CONTEXT.** НЕ давать локальной модели. Требуют понимания нескольких файлов, контрактов данных, роутинга или бизнес-логики SAR. Оставлены для более сильной модели / человека.
## Жёсткие исключения (соблюдены во всём файле)
Ни одна ATOMIC-задача не трогает:
- ❌ алгоритм зонального скоринга (7 факторов) — весь `backend/services/scoring_service.py` вынесен в раздел B;
- ❌ схему БД / миграции (`backend/models.py`, `backend/migrations/*`);
- ❌ auth/security (`backend/routers/auth.py`, JWT, bcrypt, роли);
- ❌ бизнес-логику МЧС/SAR (коэффициенты дистанции, поведенческие профили, психотипы как методику).
---
# РАЗДЕЛ A — ATOMIC (для локальной LLM)
Отсортировано по модулю. Внутри модуля — по порядку файла.
---
## A1 — `backend/services/distance_service.py` · защита от None
- **Файл / строки:** `backend/services/distance_service.py:111-121`, `148-158`, `168-178`
- **Тип:** missing validation
- **Сложность:** atomic
**PROMPT (скопировать в модель целиком):**
````
Ты правишь один Python-файл. Ниже три функции. У каждой первый аргумент — строка, и сразу вызывается `.lower()`. Если аргумент окажется None, будет AttributeError. Добавь защиту: если аргумент None или пустой, используй значение по умолчанию (для terrain → вернуть 0.5; для time_of_day → вести себя как 'день' → вернуть 1.0; для weather → вести себя как отсутствие осадков → вернуть 1.0). Не меняй числовые коэффициенты и словари. Верни ТОЛЬКО три изменённые функции целиком, в блоке ```python.
def get_terrain_coefficient(terrain: str) -> float:
terrain_lower = terrain.lower()
terrain_map = {
'лесная дорога': 0.8, 'сложный лес': 0.25, 'густой лес': 0.25,
'простой лес': 0.5, 'лес': 0.5, 'дорога': 0.8, 'тропа': 0.8,
'болото': 0.2, 'поле': 0.9, 'луг': 0.9, 'город': 1.0,
'населённый пункт': 1.0, 'горы': 0.3, 'овраг': 0.3
}
for key, value in terrain_map.items():
if key in terrain_lower:
return value
return 0.5
def get_time_of_day_coefficient(time_of_day: str) -> float:
time_lower = time_of_day.lower()
if 'ночь' in time_lower:
return 0.5
elif 'сумерки' in time_lower or 'вечер' in time_lower:
return 0.5
else:
return 1.0
def get_weather_coefficient(weather: str) -> float:
weather_lower = weather.lower()
if 'ливень' in weather_lower or 'сильный дождь' in weather_lower:
return 0.6
elif 'дождь' in weather_lower:
return 0.8
elif 'туман' in weather_lower:
return 0.7
elif 'снег' in weather_lower or 'метель' in weather_lower:
return 0.6
elif 'жара' in weather_lower:
return 0.8
else:
return 1.0
````
**Критерий приёмки:**
- `get_terrain_coefficient(None) == 0.5`, `get_time_of_day_coefficient(None) == 1.0`, `get_weather_coefficient(None) == 1.0`.
- Все существующие значения не изменились: `get_terrain_coefficient('густой лес') == 0.25`, `get_weather_coefficient('туман') == 0.7`.
- `PYTHONPATH=/root/vector python -m pytest backend/tests/test_distance_service.py -q` — зелёный.
---
## A2 — `backend/services/claude_service.py` · заменить `print()` на логирование
- **Файл / строки:** `backend/services/claude_service.py:66`, `111`, `247` (и добавить импорт/логгер вверху файла, строки 5-9)
- **Тип:** missing error handling (диагностика в stdout вместо логгера)
- **Сложность:** atomic
**PROMPT:**
````
В Python-файле три места пишут диагностику через print(). Замени их на стандартный модуль logging. Вверху файла (рядом с существующими импортами `import os`, `import json`) добавь `import logging` и создай модульный логгер `logger = logging.getLogger(__name__)`. Затем замени каждый print на соответствующий вызов logger. Для строк с ошибками используй logger.warning(...). Не меняй никакую другую логику. Верни ТОЛЬКО unified diff (формат `diff`), затрагивающий импорты и три строки.
Текущие строки:
(строка 66) — внутри except при недоступности Claude API:
print(f"Claude API unavailable: {e}. Using fallback scoring service.")
(строка 111) — внутри except geo/scoring в analyze_with_claude:
print(f"Geo/scoring service error: {e}")
(строка 247) — внутри except geo/scoring в analyze_with_fallback:
print(f"Geo/scoring service error in fallback: {e}")
Существующие импорты вверху файла:
import os
import json
from typing import Dict, List, Optional
from pydantic import BaseModel
import httpx
````
**Критерий приёмки:**
- `grep -n "print(" backend/services/claude_service.py` → пусто.
- В начале файла есть `import logging` и `logger = logging.getLogger(__name__)`.
- `PYTHONPATH=/root/vector python -c "import backend.services.claude_service"` — без ошибок.
---
## A3 — `backend/services/claude_service.py` · устаревший ID модели Claude
- **Файл / строки:** `backend/services/claude_service.py:157`
- **Тип:** bug (несуществующая/устаревшая модель)
- **Сложность:** atomic
**PROMPT:**
````
В Python-файле в теле POST-запроса к Anthropic API указан устаревший идентификатор модели. Замени строковое значение модели с "claude-sonnet-4-20250514" на "claude-sonnet-5". Больше НИЧЕГО не меняй (max_tokens, заголовки, структуру messages оставь как есть). Верни ТОЛЬКО одну изменённую строку.
Текущая строка (157):
"model": "claude-sonnet-4-20250514",
````
**Критерий приёмки:**
- Строка 157 содержит `"model": "claude-sonnet-5",`.
- `grep -n "claude-sonnet-4" backend/services/claude_service.py` → пусто.
> Примечание для проверяющего: актуальные ID на дату анализа — `claude-sonnet-5` (баланс скорость/качество), `claude-opus-4-8` (максимум качества). Для JSON-анализа кейса `claude-sonnet-5` — разумный дефолт.
---
## A4 — `backend/routers/analyze.py` · устаревший `datetime.utcnow()`
- **Файл / строки:** `backend/routers/analyze.py:3` (импорт) и `:106`
- **Тип:** bug (deprecated API, начиная с Python 3.12 `datetime.utcnow()` deprecated)
- **Сложность:** atomic
**PROMPT:**
````
В Python-файле используется устаревший `datetime.utcnow()`. Замени его на timezone-aware вариант `datetime.now(timezone.utc)`. Для этого:
1. В строке импорта `from datetime import datetime` добавь `timezone`: `from datetime import datetime, timezone`.
2. Замени `datetime.utcnow().isoformat()` на `datetime.now(timezone.utc).isoformat()`.
Больше ничего не меняй. Верни ТОЛЬКО две изменённые строки с указанием, какую на какую.
Текущий импорт (строка 3):
from datetime import datetime
Текущая строка (106), внутри словаря result:
'analyzed_at': datetime.utcnow().isoformat(),
````
**Критерий приёмки:**
- `grep -n "utcnow" backend/routers/analyze.py` → пусто.
- Импорт содержит `timezone`.
- `PYTHONPATH=/root/vector python -c "import backend.routers.analyze"` — без ошибок.
---
## A5 — `backend/main.py` · миграция `@app.on_event('startup')` → lifespan
- **Файл / строки:** `backend/main.py:38-40`
- **Тип:** bug (deprecated в текущих версиях FastAPI/Starlette)
- **Сложность:** atomic
**PROMPT:**
````
В FastAPI-приложении используется устаревший декоратор `@app.on_event('startup')`. Перепиши его на современный lifespan-контекст. Требования:
1. Добавь `from contextlib import asynccontextmanager` в импорты.
2. Создай асинхронную функцию lifespan, которая на старте вызывает init_db(), затем yield.
3. Передай её в конструктор: `app = FastAPI(title='Vector API', version='0.1.0', lifespan=lifespan)`.
4. Удали старый блок `@app.on_event('startup')` / `def startup()`.
Функция lifespan должна быть объявлена ДО создания app. init_db уже импортирован как `from backend.database import init_db`. Верни ТОЛЬКО изменённые фрагменты в блоке ```python.
Текущий релевантный код:
app = FastAPI(title='Vector API', version='0.1.0')
app.add_middleware(
CORSMiddleware,
allow_origins=cors_origins,
allow_credentials=allow_credentials,
allow_methods=['*'],
allow_headers=['*'],
)
@app.on_event('startup')
def startup() -> None:
init_db()
````
**Критерий приёмки:**
- `grep -n "on_event" backend/main.py` → пусто.
- Есть `@asynccontextmanager` и `lifespan=lifespan` в вызове `FastAPI(...)`.
- `PYTHONPATH=/root/vector python -c "import backend.main"` — без ошибок; приложение стартует, `GET /api/v1/health` → `{"status":"ok"}`.
---
## A6 — `backend/routers/admin.py` · обработка ошибок парсинга .docx
- **Файл / строки:** `backend/routers/admin.py:28-38` (функция `_extract_docx_text`) и `:134-139` (эндпоинт `admin_parse_doc`)
- **Тип:** missing error handling
- **Сложность:** atomic
> Не трогает auth: декоратор роутера и `require_roles` остаются без изменений.
**PROMPT:**
````
В этом FastAPI-роутере функция `_extract_docx_text` разбирает .docx (это zip с XML). Если загружен НЕ .docx (битый файл, не-zip, нет word/document.xml), `zipfile.ZipFile` или `archive.read` бросят исключение, и клиент получит непонятную 500-ю. Оберни разбор так, чтобы при ошибке чтения архива/XML эндпоинт возвращал HTTP 400 с понятным сообщением.
Сделай ДВА изменения, не трогая ничего другого:
1. В функции `_extract_docx_text` оберни блок чтения zip и парсинга XML в try/except (перехватывай `zipfile.BadZipFile`, `KeyError`, `ET.ParseError`). При любой из этих ошибок брось `ValueError("Не удалось прочитать .docx: файл повреждён или имеет неверный формат")`.
2. В эндпоинте `admin_parse_doc` оберни вызов `_extract_docx_text(content)` в try/except ValueError и при ошибке брось `raise HTTPException(status_code=400, detail=str(e))`.
`HTTPException` уже импортирован. `zipfile`, `ET` (xml.etree.ElementTree) уже импортированы. Верни ТОЛЬКО две изменённые функции целиком в блоке ```python.
Текущий код:
def _extract_docx_text(content: bytes) -> str:
with zipfile.ZipFile(BytesIO(content)) as archive:
xml = archive.read('word/document.xml')
root = ET.fromstring(xml)
ns = {'w': 'http://schemas.openxmlformats.org/wordprocessingml/2006/main'}
paragraphs: list[str] = []
for paragraph in root.findall('.//w:body/w:p', ns):
texts = [node.text for node in paragraph.findall('.//w:t', ns) if node.text]
if texts:
paragraphs.append(''.join(texts).strip())
return '\n'.join(paragraphs).strip()
@router.post('/parse-doc', response_model=ParseDocResponse)
async def admin_parse_doc(file: UploadFile = File(...)) -> dict:
content = await file.read()
raw_text = _extract_docx_text(content)
preview = _coerce_preview(raw_text)
return {'filename': file.filename, 'parsed': True, 'preview': preview, 'raw_text': raw_text}
````
**Критерий приёмки:**
- Загрузка валидного .docx по-прежнему возвращает 200 и корректный `preview` (тест `backend/tests/test_parse_doc_api.py` зелёный).
- Загрузка мусорного файла (не-zip) → HTTP 400 с сообщением, а не 500.
- `PYTHONPATH=/root/vector python -m pytest backend/tests/test_parse_doc_api.py -q` — зелёный.
---
## A7 — тесты: включить `pytest-asyncio` (26 пропущенных тестов)
- **Файл / строки:** `backend/requirements.txt:15` (добавить строку) + новый файл `pytest.ini` в корне репозитория
- **Тип:** missing test (тесты есть, но молча пропускаются)
- **Сложность:** atomic
**PROMPT:**
````
В проекте есть async-тесты, помеченные @pytest.mark.asyncio, но плагин pytest-asyncio не установлен, поэтому 26 тестов молча пропускаются. Нужно:
1. В конец файла requirements (см. содержимое ниже) добавить строку: pytest-asyncio==0.24.0
2. Создать НОВЫЙ файл pytest.ini в корне репозитория со следующим содержимым (включает автоматический режim asyncio и регистрирует маркер):
[pytest]
asyncio_mode = auto
testpaths = backend/tests
markers =
asyncio: async test powered by pytest-asyncio
Верни: (а) финальную строку, которую добавить в requirements.txt; (б) полное содержимое нового файла pytest.ini.
Текущий requirements.txt (последние строки):
pydantic==2.10.3
pydantic-settings==2.6.1
email-validator==2.1.0
psycopg2-binary==2.9.12
pytest==8.3.4
````
**Критерий приёмки:**
- После `pip install pytest-asyncio==0.24.0` (внутри `.venv`) и добавления `pytest.ini`:
`PYTHONPATH=/root/vector python -m pytest backend/tests -q` показывает **0 skipped** по причине "async def not natively supported" (число passed вырастает с 126 в сторону ~150).
- Никаких новых падений; предупреждение `PytestUnknownMarkWarning` для `asyncio` исчезает.
---
## A8 — удаление мёртвого кода backend (механическое)
- **Файлы (удалить целиком):**
- `backend/api/v1/analyze.py`, `backend/api/v1/auth.py`, `backend/api/v1/cases.py`, `backend/api/v1/stats.py`, `backend/api/v1/__init__.py`, `backend/api/v1/analyze.py.backup`, и пустой каталог `backend/api/` (весь `api/` — старая копия, `main.py` подключает только `backend/routers/*`)
- `backend/models_new.py`, `backend/models_updated.py`, `backend/models_backup.py` (используется только `backend/models.py`)
- `backend/services/geo_service.py.bak`, `backend/services/scoring_service.py.bak`
- `backend/check_model.py` (отладочный скрипт с битым `import models`)
- **Тип:** dead code
- **Сложность:** atomic (чисто механическое удаление — не требует чтения кода)
> Подтверждено анализом: `main.py` импортирует `backend.routers.*`; ничего вне каталога `backend/api/` его не импортирует; `models_*.py` не импортируются нигде; `.bak/.backup` — резервные копии.
**PROMPT (это shell-задача, не правка кода):**
````
Выполни удаление перечисленных файлов и проверь, что тесты не сломались. Не удаляй ничего сверх списка.
git rm backend/api/v1/analyze.py backend/api/v1/auth.py backend/api/v1/cases.py backend/api/v1/stats.py backend/api/v1/__init__.py backend/api/v1/analyze.py.backup
git rm backend/api/__init__.py
git rm backend/models_new.py backend/models_updated.py backend/models_backup.py
git rm backend/services/geo_service.py.bak backend/services/scoring_service.py.bak
git rm backend/check_model.py
Затем:
PYTHONPATH=/root/vector python -m pytest backend/tests -q
````
**Критерий приёмки:**
- Перечисленные файлы отсутствуют; каталог `backend/api/` удалён.
- `PYTHONPATH=/root/vector python -m pytest backend/tests -q` — то же число passed, что и до удаления (никаких новых ошибок импорта).
- `PYTHONPATH=/root/vector python -c "import backend.main"` — без ошибок.
---
## A9 — документация: единый адрес и имя БД
- **Файлы / строки:** `PSYCHOTYPE_SUMMARY.md` (везде `192.168.0.99`), `SERVICES_README.md`, `PSYCHOTYPE_API.md`, `MOBILE_FORM_README.md` (адреса `.99`); `DEPLOY.md` (имя БД `sar_mchs` и путь `/root/sar-mchs`)
- **Тип:** naming / консистентность документации
- **Сложность:** atomic
> Канон (сверено с `docker-compose.yml` и реальным LXC): адрес **192.168.0.108**, БД **vector_mchs**, путь проекта **/root/vector**.
**PROMPT:**
````
Задача — привести документацию к единым значениям. В markdown-файлах замени по всему тексту:
- 192.168.0.99 → 192.168.0.108
- имя базы данных sar_mchs → vector_mchs
- путь /root/sar-mchs → /root/vector
- в командах `cd /root/sar-mchs` → `cd /root/vector`
Это простая текстовая замена, кода не касается. Выполни sed-заменой по указанным .md-файлам и верни список выполненных команд:
sed -i 's/192\.168\.0\.99/192.168.0.108/g' PSYCHOTYPE_SUMMARY.md SERVICES_README.md PSYCHOTYPE_API.md MOBILE_FORM_README.md
sed -i 's/sar_mchs/vector_mchs/g; s#/root/sar-mchs#/root/vector#g' DEPLOY.md
````
**Критерий приёмки:**
- `grep -rn "192.168.0.99\|sar_mchs\|sar-mchs" *.md` → пусто.
- Смысл документов не изменился, только адреса/имена.
---
## A10 — `frontend/src/pages/analysis/AnalysisResult.jsx` · неверный парсинг времени суток
- **Файл / строки:** `frontend/src/pages/analysis/AnalysisResult.jsx:42-48`
- **Тип:** bug
- **Сложность:** atomic
**PROMPT:**
````
В React-компоненте функция getTimeOfDay ожидает строку "HH:MM" и берёт часы через lostTime.split(':')[0]. Но на вход приходит значение из <input type="datetime-local"> в формате "YYYY-MM-DDTHH:MM" (например "2026-07-21T14:30"). Тогда split(':')[0] === "2026-07-21T14", а parseInt даёт 2026 → время суток определяется неверно почти всегда.
Почини getTimeOfDay так, чтобы она корректно извлекала час из обоих форматов:
- если строка содержит 'T' — берём часть после 'T', затем часы до ':';
- иначе — берём часы до ':' как раньше.
Логика диапазонов (день/сумерки/ночь) и возвращаемые строки не меняются. Верни ТОЛЬКО исправленную функцию в блоке ```jsx.
Текущий код:
const getTimeOfDay = (lostTime) => {
if (!lostTime) return 'день';
const hour = parseInt(lostTime.split(':')[0]);
if (hour >= 6 && hour < 18) return 'день';
if (hour >= 18 && hour < 22) return 'сумерки';
return 'ночь';
};
````
**Критерий приёмки:**
- `getTimeOfDay("2026-07-21T14:30")` → `'день'`; `getTimeOfDay("2026-07-21T23:00")` → `'ночь'`; `getTimeOfDay("2026-07-21T20:00")` → `'сумерки'`.
- `getTimeOfDay("08:15")` (старый формат) → `'день'`; `getTimeOfDay("")` → `'день'`.
---
## A11 — `frontend/src/components/CaseForm/Step4Environment.jsx` · неверный номер шага в заголовке
- **Файл / строки:** `frontend/src/components/CaseForm/Step4Environment.jsx:58`
- **Тип:** bug (текст UI противоречит динамическому индикатору «Шаг X из Y»)
- **Сложность:** atomic
**PROMPT:**
````
В React-компоненте захардкожен заголовок "Шаг 5: Среда и местность", но этот компонент рендерится как шаг 4 (в родителе CaseForm он идёт под индексом, отображаемым как «Шаг 4»). Заголовок вводит в заблуждение и конфликтует с динамическим индикатором. Убери жёсткий номер шага из заголовка, оставив только название раздела.
Замени строку:
<h2>Шаг 5: Среда и местность</h2>
на:
<h2>Среда и местность</h2>
Верни ТОЛЬКО изменённую строку.
````
**Критерий приёмки:**
- Строка 58 больше не содержит «Шаг 5».
- Никакой другой разметки не тронуто.
---
## A12 — `frontend/src/components/CaseForm/Step4Environment.jsx` · обработка ошибок геолокации
- **Файл / строки:** `frontend/src/components/CaseForm/Step4Environment.jsx:28-54`
- **Тип:** missing error handling (используется блокирующий `alert`, нет структурного состояния ошибки)
- **Сложность:** atomic
**PROMPT:**
````
В React-компоненте функция handleGetLocation сообщает об ошибках геолокации через блокирующий alert(). Замени alert на запись сообщения об ошибке в состояние формы через updateData (ключ gps_error), чтобы UI мог показать ошибку не блокируя поток. Требования:
- при отсутствии navigator.geolocation: updateData({ gps_error: 'Геолокация не поддерживается вашим браузером' }); return;
- перед запросом координат сбрасывай ошибку: updateData({ gps_loading: true, gps_error: null });
- в success-колбэке помимо координат ставь gps_error: null;
- в error-колбэке: updateData({ gps_error: 'Ошибка получения координат: ' + error.message, gps_loading: false });
Не удаляй gps_loading, не меняй параметры getCurrentPosition (enableHighAccuracy/timeout/maximumAge). Верни ТОЛЬКО изменённую функцию handleGetLocation в блоке ```jsx.
Текущий код:
const handleGetLocation = () => {
if (!navigator.geolocation) {
alert("Геолокация не поддерживается вашим браузером");
return;
}
updateData({ gps_loading: true });
navigator.geolocation.getCurrentPosition(
(position) => {
updateData({
tnp_lat: position.coords.latitude.toFixed(6),
tnp_lon: position.coords.longitude.toFixed(6),
gps_loading: false
});
},
(error) => {
alert("Ошибка получения координат: " + error.message);
updateData({ gps_loading: false });
},
{
enableHighAccuracy: true,
timeout: 10000,
maximumAge: 0
}
);
};
````
**Критерий приёмки:**
- В функции больше нет `alert(`.
- При ошибке/отказе в правах в состоянии формы появляется `gps_error` с текстом; при успехе `gps_error === null`.
- Координаты по-прежнему пишутся в `tnp_lat`/`tnp_lon` с 6 знаками.
---
## A13 — `frontend/src/components/SearchMap.jsx` · CDN-иконки и рассинхрон версии Leaflet
- **Файл / строки:** `frontend/src/components/SearchMap.jsx:186-187`
- **Тип:** bug (внешние `raw.githubusercontent`/`cdnjs`, версия 1.7.1 против 1.9.4 в остальном коде)
- **Сложность:** atomic
**PROMPT:**
````
В React-компоненте иконки маркеров Leaflet грузятся с ненадёжных внешних CDN (raw.githubusercontent.com и cdnjs) и с версией 1.7.1, тогда как остальной код использует unpkg leaflet 1.9.4. Замени оба URL на unpkg-адреса версии 1.9.4 (стандартный маркер Leaflet), чтобы источник был единым и стабильным.
Замени:
iconUrl: 'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-2x-red.png',
shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-shadow.png',
на:
iconUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon-2x.png',
shadowUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-shadow.png',
Верни ТОЛЬКО две изменённые строки.
````
**Критерий приёмки:**
- В строках 186-187 нет `raw.githubusercontent.com` и `cdnjs.cloudflare.com`.
- Оба URL указывают на `unpkg.com/leaflet@1.9.4`.
- Карта на странице анализа отображает маркеры (визуальная проверка).
---
## A14 — `frontend/src/components/CaseForm/CaseForm.jsx` · убрать отладочный `console.log`
- **Файл / строки:** `frontend/src/components/CaseForm/CaseForm.jsx:119`
- **Тип:** dead code
- **Сложность:** atomic
**PROMPT:**
````
В React-компоненте в боевом пути отправки формы остался отладочный вывод. Удали строку:
console.log("Case created:", result);
Ничего кроме этой строки не меняй. Верни подтверждение и номер удалённой строки.
````
**Критерий приёмки:**
- `grep -n "console.log" frontend/src/components/CaseForm/CaseForm.jsx` → пусто.
- Логика отправки/навигации не изменена.
---
## A15 — `frontend/src/pages/AdminDashboard.jsx` · молча проглоченная ошибка загрузки дашборда
- **Файл / строки:** `frontend/src/pages/AdminDashboard.jsx:98-121`
- **Тип:** missing error handling
- **Сложность:** atomic
**PROMPT:**
````
В React-компоненте эффект загрузки дашборда при ошибке просто ставит dashboard=null и не показывает пользователю ничего (в отличие от загрузчика кейсов, который вызывает setError). Добавь пользовательское сообщение об ошибке через уже существующий сеттер setError.
В catch-блоке эффекта загрузки дашборда, внутри `if (err.name !== 'AbortError') { ... }`, помимо `setDashboard(null);` добавь `setError(err.message || 'Ошибка загрузки дашборда');`. Ничего другого не меняй. Верни ТОЛЬКО изменённый catch/finally блок в ```jsx.
Текущий код:
} catch (err) {
if (err.name !== 'AbortError') {
setDashboard(null);
}
} finally {
setDashboardLoading(false);
}
````
**Критерий приёмки:**
- При падении запроса дашборда в состоянии выставляется `error` с сообщением.
- `setDashboard(null)` сохранён; `AbortError` по-прежнему игнорируется.
> Проверяющему: убедиться, что `setError` в этом компоненте — общий стейт с загрузчиком кейсов и его отображение не конфликтует. Если нужно раздельное сообщение — см. B-раздел (рефакторинг стейта AdminDashboard).
---
## A16 — `frontend/src/App.js` · `isMobile` не реагирует на ресайз (опционально)
- **Файл / строки:** `frontend/src/App.js:30`
- **Тип:** simple refactor
- **Сложность:** atomic
**PROMPT:**
````
В React-приложении признак мобильного вычисляется один раз при монтировании и не обновляется при ресайзе/повороте экрана:
const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent) || window.innerWidth < 768;
Замени на React-состояние с подпиской на событие resize. Используй хуки useState и useEffect (проверь, что они импортированы из 'react'; если нет — добавь в существующий импорт). Реализация:
const [isMobile, setIsMobile] = useState(
() => /iPhone|iPad|iPod|Android/i.test(navigator.userAgent) || window.innerWidth < 768
);
useEffect(() => {
const onResize = () => setIsMobile(
/iPhone|iPad|iPod|Android/i.test(navigator.userAgent) || window.innerWidth < 768
);
window.addEventListener('resize', onResize);
return () => window.removeEventListener('resize', onResize);
}, []);
Не меняй, как isMobile передаётся дальше. Верни изменённый фрагмент и, при необходимости, обновлённую строку импорта из 'react', в блоке ```jsx.
````
**Критерий приёмки:**
- `isMobile` пересчитывается при изменении ширины окна (проверка в DevTools: сузить окно < 768 → рендерится мобильный путь без перезагрузки).
- Слушатель события снимается в cleanup (нет утечки).
- Импорт из `react` содержит `useState` и `useEffect`.
---
# РАЗДЕЛ B — NEEDS-CONTEXT (не давать локальной модели)
Требуют понимания нескольких файлов, контрактов данных, роутинга или бизнес-логики. Оставлено для сильной модели / человека.
---
## B1 — Весь `backend/services/scoring_service.py` (ядро: 7 факторов)
**Тип:** исключено по правилам. Любые правки (в т.ч. неиспользуемый импорт `Optional` на строке 6) делать вручную: файл — ядро зонального скоринга, риск задеть коэффициенты. Отдельно: неиспользуемый `Optional` — тривиально, но НЕ делегировать.
## B2 — `backend/services/claude_service.py:174-181` · хрупкий парсинг JSON из ответа Claude
**Тип:** missing error handling. `content.split("```json")[1]...` упадёт при ином форматировании ответа модели; `json.loads` без try/except уронит весь анализ. Нужно устойчивое извлечение JSON + graceful fallback на scoring — требует понимания контракта `AnalysisResult` и того, как `analyze_case` переключается на fallback.
## B3 — `backend/services/stats_service.py:19-39` vs `backend/routers/stats.py:19-35` · дублированная логика скоринга рекомендаций
**Тип:** simple refactor, но needs-context. Две почти одинаковые реализации `score`-логики (одна в сервисе, одна прямо в роутере) с разными правилами. Свести к одной — требует решения, какая версия «правильная», и затрагивает контракт `RecommendationRequest`.
## B4 — Удаление/подключение мёртвых frontend-компонентов
**Тип:** dead code, needs-context (продуктовое решение «удалить или доподключить»).
Не подключены ни к одному маршруту (цепочка `index.js → App.js`):
- `components/MobileForm.jsx` **и** `components/MobileForm.tsx` (дубликаты друг друга; ни один не импортируется — живая мобильная форма это `CaseForm`), `components/MobileForm.css`
- `components/BehavioralProfile.tsx` + `.css`
- `components/AnalysisLayout/*`, `components/MapView/*`, `components/ResultPanel/*` (MapView/ResultPanel импортируются только из неиспользуемого AnalysisLayout)
- `components/admin/CasesList*`, `components/admin/CaseDetail*`, `components/admin/Statistics*`, `components/admin/HeatmapView*` — `AdminDashboard` не содержит вложенных `<Routes>`, поэтому весь набор недостижим
- `components/CaseForm/Step5Resources.jsx` — не импортируется в `CaseForm.jsx` (рендерятся только Step1-4 + Step2b)
Решение (удалить vs довести маршрутизацию `/admin/*` и шаг ресурсов) — за человеком. Внутри этих файлов есть свои баги (напр. опечатка `isPsycotypeSelected` в MobileForm.tsx:269, захардкоженные URL `http://192.168.0.99:8000` в MobileForm.jsx:49 и HeatmapView.jsx:73, NaN-гварды в ResultPanel), но чинить их бессмысленно, пока не решён вопрос удаления.
## B5 — Рассинхрон контрактов данных между тремя поддоменами (desktop/mobile/admin)
**Тип:** bug, needs-context. Одни и те же поля кодируются по-разному:
- **gender:** `'мужской'/'женский'` (MobileForm.jsx) vs `'male'/'female'` (MobileForm.tsx, admin) vs `'М'/'Ж'` (Step1Child.jsx) vs `'m'/'f'` (backend `_normalize_case_data`)
- **ключи психотипа:** `unfamiliar_behavior`/`leadership` (Step2bPsychotype.jsx, и так же ждёт backend `detect_psychotype`) vs `unfamiliar_env`/`group_role` (MobileForm, admin CaseDetail) — из-за этого ответы психотипа десктоп-формы не отображаются в админке
- **транспорт:** `'велосипед'/'автомобиль'` (BehavioralProfile.tsx) vs `'bike'/'scooter'/'other'/'none'` (Step2Health.jsx) vs профили `'велосипед'/'самокат'` (backend scoring)
Нужна единая согласованная схема через границу frontend↔backend — сознательное проектное решение.
## B6 — `frontend/src/components/CaseForm/CaseForm.jsx:90-129` · навигация через `window.location.href`
**Тип:** bug, needs-context. После создания кейса используется `window.location.href = '/analysis/${id}'` — полная перезагрузка и потеря SPA-состояния, хотя приложение уже в `<Router>`. Замена на `useNavigate` затрагивает роутинг и структуру компонента.
## B7 — `frontend/src/pages/AdminDashboard.jsx:58-96` · повторная загрузка из-за `selectedCaseId` в deps
**Тип:** bug, needs-context. Эффект загрузки списка зависит от `selectedCaseId` и сам же его выставляет внутри → двойной fetch при первом рендере и после сброса фильтров. Исправление требует понимания взаимодействия эффектов и стейта дашборда.
## B8 — `note` vs `notes` — рассогласование имени поля
**Тип:** naming/bug, needs-context. Frontend (`AdminDashboard.jsx:215-216`, `handleSave`) и backend (`schemas.py` имеет и `note`, и `notes`; роутеры вручную маппят `note→notes`) по-разному называют одно поле. Нужно унифицировать по всей цепочке (schema + роутеры + фронт) — контрактное изменение.
## B9 — Отсутствие тестов и валидации пропсов на фронтенде
**Тип:** missing test / missing PropTypes, needs-context. Под `frontend/src` нет ни одного теста; ни один `.jsx`-компонент не использует PropTypes. Расчёт зон/времени в `AnalysisResult.jsx` и многошаговая валидация `CaseForm` не покрыты. Объём — целая инициатива, не атомарная задача.
## B10 — Секреты и деплой (`.env`)
**Тип:** security/deploy, исключено из делегирования. `.env` содержит плейсхолдеры `ANTHROPIC_API_KEY=your_...` и слабый `JWT_SECRET=your_jwt_secret_here_change_in_production`. Установка реального ключа Claude (для выхода из fallback) и генерация стойкого `JWT_SECRET` — ручная операция с секретами.
---
# Сводка
| Категория | Кол-во | Куда |
|---|---|---|
| Atomic (backend) | A1–A9 | локальной LLM |
| Atomic (frontend) | A10–A16 | локальной LLM |
| Needs-context | B1–B10 | сильной модели / человеку |
**Рекомендуемый порядок для локальной модели:** A8 и A7 первыми (чистка мёртвого кода и зелёный CI дают чистую базу для проверки остальных), затем backend A1–A6, затем docs A9, затем frontend A10–A16. После каждой backend-задачи прогонять `PYTHONPATH=/root/vector python -m pytest backend/tests -q`.