Files
ha-text-ai/docs/specs/2026-03-12-v2.4.0-fix-plan.md
T
SMKRV ce0a75f219 refactor: Phase 0 — extract utils.py and providers.py, centralize provider dispatch
- Create utils.py: normalize_name, get_file_hash, safe_log_data
- Create providers.py: PROVIDER_REGISTRY with get_default_endpoint,
  get_default_model, build_auth_headers
- Add DEFAULT_ANTHROPIC_MODEL constant (was incorrectly using gpt-4o-mini)
- Replace all inline dispatch tables in config_flow.py and __init__.py
- Fix circular import: coordinator.py now imports from utils, not config_flow
- Fix NameError in error paths: replace bare constant refs with provider functions
- Raise ValueError for unknown providers instead of silent OpenAI fallback
2026-03-12 00:55:59 +03:00

852 lines
33 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.
# ha-text-ai v2.4.0 — Полный план исправлений
**Дата**: 2026-03-12
**Версия**: 2.3.0 → 2.4.0
**Принцип**: Без breaking changes. Один релиз.
**Статус**: В работе
---
## Оглавление
- [Принципы обратной совместимости](#принципы-обратной-совместимости)
- [Workflow разработки](#workflow-разработки)
- [Фаза 0: Подготовка инфраструктуры](#фаза-0-подготовка-инфраструктуры)
- [Фаза 1: Security Hardening](#фаза-1-security-hardening)
- [Фаза 2: Critical Bug Fixes](#фаза-2-critical-bug-fixes)
- [Фаза 3: Concurrency & State](#фаза-3-concurrency--state)
- [Фаза 4: Dead Code & Deduplication](#фаза-4-dead-code--deduplication)
- [Фаза 5: Coordinator Refactoring](#фаза-5-coordinator-refactoring)
- [Фаза 6: UI/UX & Translations](#фаза-6-uiux--translations)
- [Финал: Full Review + Tests + CI/CD](#финал-full-review--tests--cicd)
- [Трекинг прогресса](#трекинг-прогресса)
---
## Принципы обратной совместимости
| Что | Правило |
|-----|---------|
| Sensor attributes | Все существующие сохраняются. Новые добавляются. Избыточные остаются для совместимости |
| Service response format | Структура ответа `ask_question`, `get_history` не меняется |
| Service schema | Поля не удаляются, не переименовываются. Новые — только `vol.Optional` |
| Entity ID | Формат `sensor.ha_text_ai_{name}` не меняется |
| Config entry data | Добавление новых ключей OK. Существующие не удаляются. Миграция прозрачная |
| History file format | JSON-структура сохраняется. Новые поля — опциональные |
| `hass.data[DOMAIN]` | Ключи по `entry_id` — не менять |
---
## Workflow разработки
```
Для каждой фазы:
1. Реализация фикса
2. 4 параллельных review-агента (Code, Security, Architecture, UI/UX)
3. Документация
4. Исправление замечаний review
5. Повторная проверка (4 агента)
6. Документация
7. Локальный коммит
После ВСЕХ фаз:
1. Полный финальный review (все 4 агента)
2. Добавление тестов + CI/CD workflow
3. Финальный коммит
4. Push
```
---
## Фаза 0: Подготовка инфраструктуры
**Цель**: Создать фундамент для остальных фаз без изменения поведения.
### 0.1. Создать `utils.py`
**Файл**: `custom_components/ha_text_ai/utils.py` (новый)
Извлечь из `config_flow.py`:
- Функцию `normalize_name()` (config_flow.py:351-370)
- Вспомогательную функцию `get_file_hash()` из `__init__.py:108-114`
- Хелпер для безопасного логирования (фильтрация API-ключей)
```python
def safe_log_data(data: dict, sensitive_keys: tuple = (CONF_API_KEY,)) -> dict:
"""Filter sensitive keys from data for safe logging."""
return {k: "***" if k in sensitive_keys else v for k, v in data.items()}
```
**Обратная совместимость**: `config_flow.py` сохраняет `normalize_name` как re-export:
```python
from .utils import normalize_name # backward compat
```
Это позволит `coordinator.py:29` (`from .config_flow import normalize_name`) продолжать работать, пока мы не обновим все импорты.
### 0.2. Создать `providers.py`
**Файл**: `custom_components/ha_text_ai/providers.py` (новый)
Единый реестр провайдеров вместо дублирования dispatch-таблиц в 4+ местах:
```python
PROVIDER_REGISTRY = {
API_PROVIDER_OPENAI: {
"default_model": DEFAULT_MODEL_OPENAI,
"default_endpoint": DEFAULT_ENDPOINT_OPENAI,
"auth_header": "Authorization",
"auth_prefix": "Bearer ",
"check_path": "/models",
},
API_PROVIDER_ANTHROPIC: { ... },
API_PROVIDER_DEEPSEEK: { ... },
API_PROVIDER_GEMINI: { ... },
}
def get_provider_config(provider: str) -> dict: ...
def get_default_endpoint(provider: str) -> str: ...
def get_default_model(provider: str) -> str: ...
def build_auth_headers(provider: str, api_key: str) -> dict: ...
```
### 0.3. Обновить импорты
Во всех файлах заменить:
- `from .config_flow import normalize_name``from .utils import normalize_name`
- Inline dispatch-таблицы → вызовы `providers.py`
**Коммит**: `refactor: Extract utils.py and providers.py for code deduplication`
---
## Фаза 1: Security Hardening
**Цель**: Устранить все уязвимости безопасности.
### 1.1. Убрать логирование API-ключей [CRITICAL]
**Файлы и строки**:
- `__init__.py:312``_LOGGER.debug(f"Setting up HA Text AI entry: {entry.data}")`
- `config_flow.py:160``_LOGGER.debug(f"Provider step input data: {user_input}")`
- `config_flow.py:465``_LOGGER.debug(f"Creating config entry with data: {entry_data}")`
- `sensor.py:116``_LOGGER.debug(f"Initializing sensor with config entry: {config_entry.data}")`
**Исправление**: Заменить на `safe_log_data()` из `utils.py`:
```python
_LOGGER.debug("Setting up HA Text AI entry: %s", safe_log_data(entry.data))
```
Заодно перевести все `_LOGGER` вызовы с f-string на `%s`-форматирование (lazy evaluation).
**Масштаб**: Все файлы, все `_LOGGER.debug(f"...")``_LOGGER.debug("...", arg)`.
### 1.2. Валидация endpoint — HTTPS-only + защита от SSRF [HIGH]
**Файлы**: `utils.py` (новая функция), `config_flow.py`, `__init__.py`
Добавить в `utils.py`:
```python
from urllib.parse import urlparse
import ipaddress
def validate_endpoint(url: str) -> tuple[bool, str]:
"""Validate API endpoint URL. Returns (is_valid, error_message)."""
parsed = urlparse(url)
if parsed.scheme not in ("https",):
return False, "only_https_supported"
hostname = parsed.hostname
if not hostname:
return False, "invalid_url"
try:
ip = ipaddress.ip_address(hostname)
if ip.is_private or ip.is_loopback or ip.is_link_local:
return False, "private_ip_not_allowed"
except ValueError:
pass # hostname is a domain name, not IP — OK
return True, ""
```
Применить в:
- `config_flow.py:async_step_provider` — при вводе endpoint
- `config_flow.py:OptionsFlowHandler` — при смене endpoint
- `__init__.py:async_setup_entry` — перед созданием API client
**Обратная совместимость**: Существующие записи с HTTP endpoints продолжат работать (валидация только при создании/изменении). Добавить warning в лог при загрузке существующего HTTP endpoint.
### 1.3. Не показывать API-ключ в формах [HIGH]
**Файлы**: `config_flow.py:648-650, 224, 286, 329`
**Исправление**:
- В `_get_settings_schema` и во всех error re-display формах: НЕ передавать `default=data.get(CONF_API_KEY)`.
- Вместо `vol.Required(CONF_API_KEY, default=...)` использовать `vol.Optional(CONF_API_KEY, description={"suggested_value": ""})`.
- Если поле пустое — сохранять существующий ключ.
### 1.4. Требовать повторный ввод ключа при смене endpoint [HIGH]
**Файл**: `config_flow.py:OptionsFlowHandler.async_step_settings`
При обнаружении изменения endpoint:
- Очистить поле API key
- Показать предупреждение в описании поля
- Не отправлять старый ключ на новый endpoint при валидации
### 1.5. Убрать history_path из атрибутов сенсора [MEDIUM]
**Файл**: `coordinator.py:_get_limited_history`
Удалить `"history_path": self._history_file` из возвращаемого dict. Это внутренний путь FS.
### 1.6. Санитизировать ошибки в service response [MEDIUM]
**Файл**: `__init__.py:161`
Заменить `"error": str(err)` на `"error": "Request failed"`. Полную ошибку логировать через `_LOGGER.error`.
### 1.7. Добавить лимит длины для question и json_schema [LOW]
**Файл**: `__init__.py:73,80`
```python
vol.Required("question"): vol.All(cv.string, vol.Length(max=100000)),
vol.Optional("json_schema"): vol.All(cv.string, vol.Length(max=50000)),
```
### 1.8. Валидация Gemini API key [LOW]
**Файл**: `config_flow.py:263-278`, `__init__.py:279-285`
Для Gemini: выполнять реальный API-вызов `models.list()` вместо проверки "непустая строка".
**Коммит**: `fix: Security hardening — credential protection, SSRF prevention, input validation`
---
## Фаза 2: Critical Bug Fixes
**Цель**: Исправить баги, приводящие к потере данных и некорректному поведению.
### 2.1. Исправить инициализацию координатора [CRITICAL]
**Файл**: `coordinator.py:177-186`
**Было**: 5 fire-and-forget `async_create_task` в `__init__` + `_history_file` назначается после.
**Станет**:
1. Убрать все `async_create_task` из `__init__`
2. Переместить `self._history_file` назначение ПЕРЕД задачами
3. Создать метод `async_initialize()`:
```python
async def async_initialize(self) -> None:
"""Initialize coordinator: directories, history, metrics. Must be awaited."""
await self._create_history_dir()
await self._check_history_directory()
await self._initialize_metrics()
await self.async_initialize_history_file()
await self._migrate_history_from_txt_to_json()
```
4. В `__init__.py:async_setup_entry` вызывать `await coordinator.async_initialize()` после конструктора.
### 2.2. Удалить деструктивный цикл `async_set` [CRITICAL]
**Файл**: `coordinator.py:778-781`
Удалить полностью цикл:
```python
for entity_id in self.hass.states.async_entity_ids():
if entity_id.startswith(entity_id_base):
self.hass.states.async_set(entity_id, self._get_current_state())
```
`async_request_refresh()` на строке 775 уже обновляет состояние через координатор → `_handle_coordinator_update``async_write_ha_state()`.
### 2.3. Исправить `temperature=0` [HIGH]
**Файл**: `coordinator.py:870-873`
Заменить:
```python
temp_temperature = temperature or self.temperature
temp_max_tokens = max_tokens or self.max_tokens
```
На:
```python
temp_temperature = temperature if temperature is not None else self.temperature
temp_max_tokens = max_tokens if max_tokens is not None else self.max_tokens
temp_context = context_messages if context_messages is not None else self.context_messages
temp_system = system_prompt if system_prompt is not None else self._system_prompt
```
**Файл**: `__init__.py:76`
Заменить `cv.positive_float` на `vol.All(vol.Coerce(float), vol.Range(min=0.0, max=2.0))` для temperature.
### 2.4. Исправить retry loop — не ретраить нетранзиентные ошибки [HIGH]
**Файл**: `api_client.py:86-122`
```python
# Было: except Exception ловит HomeAssistantError (4xx ответы) и ретраит
# Станет:
for attempt in range(self.retry_count):
try:
async with self.session.post(...) as response:
if response.ok:
return await response.json()
error_data = {}
try:
error_data = await response.json()
except Exception:
error_data = {"raw": await response.text()}
if response.status == 429: # rate limit — retry
await asyncio.sleep(attempt + 1)
continue
if response.status >= 400: # client/server error — don't retry
raise HomeAssistantError(f"API error: {response.status}")
except (asyncio.TimeoutError, aiohttp.ClientError) as err:
if attempt == self.retry_count - 1:
raise HomeAssistantError(f"Connection failed: {err}") from err
await asyncio.sleep(attempt + 1)
```
### 2.5. Починить ротацию истории [HIGH]
**Файл**: `coordinator.py:588-592`
`_rotate_history_files``async def`, но вызывается через `async_add_executor_job` (запускает sync в потоке → получает coroutine, не результат).
**Исправление**: Заменить:
```python
await self.hass.async_add_executor_job(self._rotate_history_files)
```
На:
```python
await self._rotate_history_files()
```
Или сделать `_rotate_history_files` синхронной (предпочтительнее — она делает файловые операции).
### 2.6. Исправить `_is_rate_limited` — сбрасывать после успеха [HIGH]
**Файл**: `coordinator.py`
В `async_process_message` после успешного API-вызова:
```python
self._is_rate_limited = False
self.endpoint_status = "ready"
self._error_count = 0
```
### 2.7. `ConnectionError` не должен ставить `is_rate_limited` [HIGH]
**Файл**: `coordinator.py:361-364`
Убрать `"is_rate_limited": True` из маппинга `ConnectionError`.
### 2.8. Исправить `async_ask_question` — propagation ошибок [MEDIUM]
**Файл**: `coordinator.py:815-849`
`async_ask_question` — бессмысленная обёртка над `async_process_question`. Убрать лишний уровень индирекции, или добавить полезную логику (валидацию входных параметров).
**Файл**: `__init__.py:148-162`
Сервис `ask_question` глотает все ошибки. Добавить `raise HomeAssistantError` для критических ошибок, сохраняя `success: False` в response data для совместимости:
```python
except Exception as err:
_LOGGER.error("ask_question failed: %s", err)
response = {"success": False, "error": "Request failed", ...}
# НЕ raise — для обратной совместимости. Ошибка в response data.
return response
```
**Коммит**: `fix: Critical bugs — init race condition, data loss, temperature=0, retry logic, history rotation`
---
## Фаза 3: Concurrency & State
**Цель**: Устранить race conditions и проблемы управления состоянием.
### 3.1. Добавить `asyncio.Lock` для конкурентных запросов [MEDIUM]
**Файл**: `coordinator.py:__init__`
```python
self._request_lock = asyncio.Lock()
```
В `async_process_question`:
```python
async with self._request_lock:
# ... existing logic
```
### 3.2. Исправить Semaphore — убрать или сделать instance-level [MEDIUM]
**Файл**: `coordinator.py:668`
Убрать `async with asyncio.Semaphore(1):``DataUpdateCoordinator` уже сериализует `_async_update_data`.
### 3.3. Исправить `async_shutdown` — правильный ключ [HIGH]
**Файл**: `coordinator.py:1159`
Заменить `self.hass.data[DOMAIN].pop(self.instance_name, None)` на:
```python
# Cleanup handled by async_unload_entry in __init__.py
# Remove this line entirely — it uses wrong key (instance_name vs entry_id)
```
### 3.4. Исправить double timeout в `_make_request` [MEDIUM]
**Файл**: `api_client.py:98-103`
Убрать `async with timeout(self.api_timeout):``aiohttp ClientTimeout` достаточно. Убрать `from async_timeout import timeout`.
### 3.5. Кэшировать Gemini client [MEDIUM]
**Файл**: `api_client.py`
```python
def __init__(self, ...):
...
self._gemini_client = None
def _get_gemini_client(self, api_key):
if self._gemini_client is None:
self._gemini_client = genai.Client(api_key=api_key, ...)
return self._gemini_client
```
### 3.6. Исправить `get_history` — не мутировать оригинальные записи [MEDIUM]
**Файл**: `coordinator.py:1118-1125`
```python
# Было: entry["metadata"] = {...} # мутирует оригинал
# Станет:
import copy
enriched = copy.deepcopy(entry)
enriched["metadata"] = {...}
```
### 3.7. Персистировать system_prompt [LOW]
**Файл**: `coordinator.py`
Сохранять `_system_prompt` в файл `.storage/ha_text_ai_system_prompt_{name}.txt`. Загружать при инициализации. При reload/restart — восстанавливать.
### 3.8. Исправить `os.rename` в async контексте [MEDIUM]
**Файл**: `coordinator.py:1047`
```python
# Было: os.rename(old_history_file, backup_file)
# Станет:
await self.hass.async_add_executor_job(os.rename, old_history_file, backup_file)
```
**Коммит**: `fix: Concurrency — request locking, state management, Gemini client caching, system prompt persistence`
---
## Фаза 4: Dead Code & Deduplication
**Цель**: Удалить мёртвый код и устранить дублирование.
### 4.1. Удалить мёртвый код
| Файл | Что удалить | Строки |
|------|-------------|--------|
| `coordinator.py` | `_check_memory_available()` + `import psutil` | 1138-1154, 17 |
| `coordinator.py` | `_sync_write_history_entry()` | 535-586 |
| `coordinator.py` | `AsyncFileHandler` class (заменить на прямой `aiofiles.open`) | 51-63 |
| `api_client.py` | `check_connection()` (всегда broken) | 326-... |
| `api_client.py` | `self._closed` flag (никто не проверяет) | в shutdown() |
| `api_client.py` | `import HomeAssistant` (не используется) | 16 |
| `api_client.py` | `import datetime, timedelta` (не используются) | 14 |
| `coordinator.py` | `import re` (не используется) | 18 |
| `__init__.py` | `import TypeVar` + `ConfigType` (не нужен) | 16, 69 |
| `__init__.py` | `import timedelta` (не используется) | 15 |
### 4.2. Консолидировать schema определения
**Файл**: `const.py:188-256`
Удалить мёртвые schema:
- `SERVICE_SCHEMA_ASK_QUESTION` (неиспользуемый, конфликтует с `__init__.py`)
- `SERVICE_SCHEMA_SET_SYSTEM_PROMPT` (дублирует `__init__.py`)
- `SERVICE_SCHEMA_GET_HISTORY` (дублирует `__init__.py`)
- `CONFIG_SCHEMA` (не используется)
Оставить единственное определение в `__init__.py`.
### 4.3. Удалить неиспользуемые зависимости из `manifest.json`
**Файл**: `manifest.json`
Удалить:
- `anthropic>=0.8.0` — не импортируется (API через `aiohttp`)
- `openai>=1.12.0` — не импортируется (API через `aiohttp`)
- `certifi>=2024.2.2` — уже есть в HA
- `async-timeout>=4.0.0` — deprecated, заменяем на `asyncio.timeout`
Обновить:
- `aiohttp>=3.9.4` (security fix: CVE-2024-23334, CVE-2024-23829)
### 4.4. Использовать `providers.py` вместо inline dispatch
Заменить во всех файлах inline-таблицы `default_endpoint`/`default_model`/headers:
**`__init__.py:326-362`**:
```python
# Было: 4 if/elif для endpoint + 4 if/elif для model + 4 if/elif для headers
# Станет:
from .providers import get_provider_config, build_auth_headers
config = get_provider_config(provider)
endpoint = entry_data.get(CONF_API_ENDPOINT) or config["default_endpoint"]
headers = build_auth_headers(provider, api_key)
```
**`config_flow.py:104-117, 487-502`**: аналогично.
### 4.5. Консолидировать `_async_validate_api`
**Файлы**: `config_flow.py` — два идентичных метода в `HATextAIConfigFlow` (372-407) и `OptionsFlowHandler` (517-549).
Извлечь в `utils.py`:
```python
async def validate_api_connection(hass, provider, endpoint, api_key) -> tuple[bool, str]:
```
### 4.6. Унифицировать timeout
Заменить все `from async_timeout import timeout` на `from asyncio import timeout` (Python 3.12+, HA 2024.12.0 требует).
**Файлы**: `api_client.py:3`, `__init__.py:19`
**Коммит**: `refactor: Remove dead code, consolidate schemas, unify provider dispatch, clean dependencies`
---
## Фаза 5: Coordinator Refactoring
**Цель**: Разбить god object `coordinator.py` (~1160 строк) на модули с чёткими ответственностями.
### 5.1. Создать `history.py`
**Файл**: `custom_components/ha_text_ai/history.py` (новый)
Извлечь из `coordinator.py`:
- `_create_history_dir()`
- `_check_history_directory()`
- `async_initialize_history_file()`
- `_migrate_history_from_txt_to_json()`
- `_write_history_entry()`
- `_rotate_history()` / `_rotate_history_files()`
- `async_clear_history()`
- `async_get_history()`
- `_update_history()`
- `_get_limited_history()`
- `_conversation_history` list
- `_request_lock` (для записи в историю)
Класс:
```python
class HistoryManager:
def __init__(self, hass, instance_name, normalized_name, history_dir, max_history_size):
...
async def async_initialize(self): ...
async def write_entry(self, entry): ...
async def get_history(self, limit, filter_model, ...): ...
async def clear(self): ...
def get_limited(self, max_entries): ...
@property
def conversation_history(self) -> list: ...
@property
def size(self) -> int: ...
```
### 5.2. Создать `metrics.py`
**Файл**: `custom_components/ha_text_ai/metrics.py` (новый)
Извлечь из `coordinator.py`:
- `_initialize_metrics()`
- `_update_metrics()`
- `_save_metrics()` / `_load_metrics()`
- `_metrics` dict
- `_total_tokens`, `_prompt_tokens`, `_completion_tokens`
- `_successful_requests`, `_failed_requests`
- `_average_latency`, `_max_latency`, `_min_latency`
Класс:
```python
class MetricsTracker:
def __init__(self, hass, normalized_name, storage_dir):
...
async def async_initialize(self): ...
def update(self, tokens_used, prompt_tokens, completion_tokens, latency): ...
async def save(self): ...
@property
def summary(self) -> dict: ...
```
### 5.3. Упростить `coordinator.py`
После извлечения `coordinator.py` сократится с ~1160 до ~400-500 строк:
```python
class HATextAICoordinator(DataUpdateCoordinator):
def __init__(self, hass, client, config, ...):
self.history = HistoryManager(...)
self.metrics = MetricsTracker(...)
...
async def async_initialize(self):
await self.history.async_initialize()
await self.metrics.async_initialize()
async def async_process_question(self, question, **kwargs):
async with self._request_lock:
...
response = await self.client.create(...)
self.metrics.update(...)
await self.history.write_entry(...)
return response
async def _async_update_data(self):
...
```
### 5.4. Обновить `sensor.py`
`sensor.py` читает данные из координатора. Обновить доступ:
```python
# Было: self.coordinator._conversation_history
# Станет: self.coordinator.history.conversation_history
```
### 5.5. Добавить лимит архивов истории
**Файл**: `history.py` (в рамках ротации)
При ротации — хранить максимум 5 архивных файлов. Удалять самые старые.
**Коммит**: `refactor: Split coordinator into coordinator, history, and metrics modules`
---
## Фаза 6: UI/UX & Translations
**Цель**: Исправить проблемы пользовательского интерфейса и переводов.
### 6.1. Исправить дублированный JSON-ключ `"provider"` [CRITICAL]
**Файлы**: Все 8 файлов в `translations/`
В каждом файле под `config.step` есть два ключа `"provider"`. Объединить в один с правильным содержимым (title + data из второго, т.к. первый перезаписывается).
### 6.2. Добавить `strings.json` [LOW]
**Файл**: `custom_components/ha_text_ai/strings.json` (новый)
Скопировать `translations/en.json``strings.json` (HA convention: `strings.json` = authoritative English source).
### 6.3. Извлечь form schema builder для config flow [HIGH]
**Файл**: `config_flow.py`
Создать `_build_provider_schema(user_input, provider, errors)` по образцу `_get_settings_schema` из `OptionsFlowHandler`. Использовать единый метод во всех 5 местах re-display формы. Все поля всегда присутствуют.
### 6.4. Использовать `NumberSelector` для числовых полей [MEDIUM]
**Файл**: `config_flow.py`
Для temperature, max_tokens, request_interval, api_timeout, context_messages, max_history_size:
```python
vol.Required(CONF_TEMPERATURE, default=DEFAULT_TEMPERATURE): selector.NumberSelector(
selector.NumberSelectorConfig(min=0.0, max=2.0, step=0.1, mode="slider")
),
```
### 6.5. Выровнять переводы с кодом
**Файлы**: Все `translations/*.json`
| Действие | Что |
|----------|-----|
| Удалить | Мёртвые error keys: `invalid_characters`, `name_too_long`, `history_storage_error`, `history_rotation_error`, `history_file_access_error`, `invalid_model`, `rate_limit`, `context_length` и др. |
| Удалить | Мёртвые state keys: `retrying`, `queued` |
| Добавить | Недостающие attribute keys для реальных атрибутов |
| Оставить | Все существующие используемые ключи |
### 6.6. Исправить ошибки в переводах
| Язык | Проблема | Исправление |
|------|----------|-------------|
| `it.json` | "Istanze" (мн.ч.) → "Istanza" (ед.ч.) | Заменить все вхождения |
| `hi.json` | "instance" = "उदाहरण" (example) | Заменить на "इंस्टैंस" |
| `sr.json` | "disconnected" = "Прекључено" (switched) | Заменить на "Искључено" |
| `de.json` | Опции селектора не переведены | Перевести: "OpenAI (kompatibel)" и т.д. |
### 6.7. Прочие UI-исправления
| # | Что | Файл |
|---|-----|------|
| A | `PLATFORMS: list[Platform] = [Platform.SENSOR]` | `const.py:20` |
| B | `supports_response=SupportsResponse.OPTIONAL` | `__init__.py:203` |
| C | `manufacturer="SMKRV"` вместо `"Community"` | `sensor.py:157` |
| D | `datetime` selector для `start_date` в services | `services.yaml:146-149` |
| E | Выровнять temperature default: 0.1 везде | `services.yaml:63` |
| F | Убрать "This service now returns..." из description | `services.yaml:6-7` |
| G | Убрать пустые массивы из manifest (`bluetooth`, `mqtt` и т.д.) | `manifest.json` |
| H | Убрать нестандартное копирование иконок в `www/` | `__init__.py:228-272` |
**Пункт H — детали**: Современные HA + HACS используют `icons/` directory напрямую. Удалить `_copy_icon_to_www()` и весь связанный код. Существующие файлы в `www/ha_text_ai/` не трогаем (пользователь может удалить сам).
### 6.8. Исправить 404 как успешную валидацию API
**Файлы**: `__init__.py:295`, `config_flow.py:399`
Только 200 = успех. 404 → `cannot_connect`.
**Коммит**: `fix: UI/UX — translations, config flow forms, sensor attributes, services.yaml alignment`
---
## Финал: Full Review + Tests + CI/CD
### F.1. Полный финальный review
Запуск всех 4 review-агентов на весь codebase:
1. Code Review
2. Security Review
3. Architectural Review
4. UI/UX Review
Исправить все найденные проблемы. Повторить до чистого прохода.
### F.2. Добавить тесты
**Файлы**: `tests/` (новая директория)
Минимальный набор тестов:
```
tests/
├── conftest.py # HA test fixtures
├── test_config_flow.py # Config flow + options flow
├── test_api_client.py # API client per provider
├── test_coordinator.py # Coordinator logic
├── test_sensor.py # Sensor entity
├── test_utils.py # Utility functions
├── test_history.py # History manager
├── test_metrics.py # Metrics tracker
└── test_providers.py # Provider registry
```
Ключевые тесты:
- Config flow: создание entry для каждого провайдера, validation errors, options flow
- API client: успешный ответ, retry на 429, НЕ retry на 400, timeout
- Coordinator: `temperature=0` работает, concurrent requests, history write/read
- Security: endpoint validation (HTTPS-only, no private IPs), API key не в логах
- History: write, read, rotate, archive cleanup
- Translations: все файлы имеют одинаковые ключи
### F.3. CI/CD workflow для тестов
**Файл**: `.github/workflows/tests.yaml` (новый)
```yaml
name: Tests
on:
push:
branches: [main, dev]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install dependencies
run: |
pip install pytest pytest-asyncio pytest-homeassistant-custom-component
pip install -r requirements_test.txt
- name: Run tests
run: pytest tests/ -v
```
### F.4. Обновить version в manifest.json
`"version": "2.3.0"``"version": "2.4.0"`
### F.5. Финальные коммиты
```
test: Add comprehensive test suite for all components
chore: Add test workflow to CI/CD
chore: Bump version to 2.4.0
```
### F.6. Push
Только после успешного прохождения всех проверок.
---
## Трекинг прогресса
| Фаза | Статус | Дата начала | Дата завершения | Коммит |
|------|--------|-------------|-----------------|--------|
| 0. Подготовка | [x] | 2026-03-12 | 2026-03-12 | e7c8b22+ |
| 1. Security | [ ] | | | |
| 2. Critical Bugs | [ ] | | | |
| 3. Concurrency | [ ] | | | |
| 4. Dead Code | [ ] | | | |
| 5. Coordinator | [ ] | | | |
| 6. UI/UX | [ ] | | | |
| F. Final Review | [ ] | | | |
| F. Tests | [ ] | | | |
| F. CI/CD | [ ] | | | |
| F. Push | [ ] | | | |
---
## Новые файлы (итого)
```
custom_components/ha_text_ai/
├── utils.py (Phase 0)
├── providers.py (Phase 0)
├── history.py (Phase 5)
├── metrics.py (Phase 5)
├── strings.json (Phase 6)
tests/
├── conftest.py (Final)
├── test_*.py (Final)
.github/workflows/
├── tests.yaml (Final)
```
## Изменяемые файлы (все фазы)
```
custom_components/ha_text_ai/
├── __init__.py (Phases 1,2,4,6)
├── api_client.py (Phases 2,3,4)
├── config_flow.py (Phases 0,1,4,6)
├── const.py (Phases 4,6)
├── coordinator.py (Phases 1,2,3,4,5)
├── sensor.py (Phases 1,5,6)
├── manifest.json (Phases 4,6,Final)
├── services.yaml (Phase 6)
├── translations/*.json (Phase 6)
```