639 lines
41 KiB
Markdown
639 lines
41 KiB
Markdown
# ВЕКТОР — очередь технического долга для делегирования
|
||
|
||
**Проект:** ВЕКТОР (Система определения приоритетных направлений поиска, 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`.
|