# MANAGEMENT_RUNBOOK.md

Единый runbook по management-командам проекта.

Документ отвечает на 3 вопроса:
- что запускать;
- когда запускать;
- как безопасно откатываться/повторять запуск.

## Каталог команд

1. `regenerate_seria_roots` — пересчет корневых серий (иерархия и консолидация).
2. `generate_map_js` — генерация JavaScript для карт с геоданными зданий.
3. `generate_sitemaps` — оффлайн генерация sitemap-файлов.
4. `regenerate_seria_prerender` — оффлайн пересборка pre-render шаблонов для `catalog_seria_info`.
5. `populate_seo_fields` — автозаполнение SEO-полей блога из существующих данных.
6. `make_rating` — пересчёт рейтингов профилей и стеклопакетов методом Манна-Уитни.

## Общие правила запуска

- Запускать команды из корня репозитория.
- Для локального/CI запуска использовать `poetry`.
- Не запускать тяжелые операции через HTTP-эндпоинты `/service/*`.
- Перезапуск веб-сервера (`gunicorn`/`uWSGI`) делать отдельным шагом оркестрации, а не из кода Django.

Базовый шаблон запуска:

```bash
cd /Users/e-serg/PRJ/2022-oknardia
poetry run python oknardia/manage.py <command> [args]
```

## 1) Команда `regenerate_seria_roots`

Назначение:
- пересчитать корневые серии (root) для всей иерархии серий домов.
- консолидировать различные написания одной и той же серии в одну "корневую" серию.

### Контекст

На разных источниках данные о типовых сериях домов были записаны с разными орфографическими вариантами.
Например серия **II-57** могла быть обозначена как:
- `2-57` (цифра вместо кириллицы)
- `И-57` (кириллица И вместо латинской II)
- `П-57` (опечатка)
- и т.п.

При парсинге данных эти варианты могли оказаться в БД как отдельные серии, хотя на самом деле это одна и та же серия.
Функция `regenerate_seria_roots` связывает все эти варианты (алиасы) с одной **корневой** серией.

### Когда это нужно

При добавлении новых адресов и серий типового строительства, при ручном редактировании иерархии серий в админке,
при загрузке новых данных с разными орфографическими вариантами серий. Кроме того, если для уже существующих в базе
серий будут получены данные о типовых размерах оконных проёмов и типов квартирах (сейчас в базе около 4500 серий, из  
них всего 31 корневых серий и 1957 серий с найденным корнем... ещё 2502 неописанных серии без корня, т.е. больше
половины, могут пополнить каталог Окнардии.

### Как это работает

1. **Этап 1**: Находит "корневые" серии (root series) — те, что реально используются в таблице `Apartment_Type` 
   (т.е. у которых есть квартиры). Для каждой такой серии устанавливает `kRoot_id = own_id`.

2. **Этап 2**: Для всех остальных серий:
   - Движется вверх по дереву иерархии (`kParent_id`)
   - Ищет корневую серию (ту, которая либо не имеет родителя, либо она в списке корневых)
   - Устанавливает найденную корневую серию в поле `kRoot_id`
   - Если не находит корневую серию → `kRoot_id = None`

### Пример структуры после обработки

```
Таблица: Seria_Info

id  | sSeriaName | kParent_id | kRoot_id  | Комментарий
----|------------|------------|-----------|-------------------
123 | II-57      | NULL       | 123       | Корневая серия
124 | 2-57       | 123        | 123       | Алиас (через родителя)
125 | И-57       | 123        | 123       | Алиас (через родителя)
126 | П-57       | 123        | 123       | Алиас (через родителя)
```

### Базовый запуск

```bash
cd /Users/e-serg/PRJ/2022-oknardia
poetry run python oknardia/manage.py regenerate_seria_roots
```

### Параметры запуска

**`--verbosity 0`** — только ошибки (минимум информации):

```bash
poetry run python oknardia/manage.py regenerate_seria_roots --verbosity 0
```

**`--verbosity 1`** — точки/плюсы (стандартный режим):

```bash
poetry run python oknardia/manage.py regenerate_seria_roots
# или явно
poetry run python oknardia/manage.py regenerate_seria_roots --verbosity 1
```

**`--verbosity 2`** — подробный вывод (названия серий):

```bash
poetry run python oknardia/manage.py regenerate_seria_roots --verbosity 2
```

**`--verbosity 3`** — очень подробный вывод в виде таблицы:

```bash
poetry run python oknardia/manage.py regenerate_seria_roots --verbosity 3
```

### Примеры вывода

**Verbosity 0 (только ошибки - чистый вывод):**
```
✅ Пересчет завершен! Время: 2.18с
```

**Verbosity 1 (магический режим - точки и символы):**
```
=== ПЕРЕСЧЕТ КОРНЕВЫХ СЕРИЙ ===
Этап 1: Ищем корневые серии в таблице квартир...
✓ Найдено корневых серий: 241
...............................

Этап 2: Главная магия - обрабатываем все серии в иерархии...
-----++..--.++--...--.+.++++.-+++++++-.-.++.-+--++--++++-+++++++-++.+--.+++-+-..++++++-++++++++++--.-++++++-++++
+--+-+++++++++-++++++++++-++++--+++.+++--+++++++++++++++++++---++.+-+-+++++-++++++++++-+++-+----+.-+++-+--++++++
+++----+--+-+++++-+--+++--+++-.+++++-++++++++-+---++-+++++---+++------++----++-+--++----+--++--++++--+++++++++++
+++++++-------++++---+++-[... очень много символов ...]

=== РЕЗУЛЬТАТЫ ===
✓ Корневых серий (обработаны на этапе 1): 31
✓ Серий с найденным корнем: 1957
⚠ Серий без корня: 2502

✅ Пересчет завершен! Время: 2.18с
```

**Легенда магического режима:**
- `.` = корневая серия (обработана на этапе 1)
- `+` = серия с найденным корнем
- `-` = серия без найденного корня
- `E` = ошибка при обработке

**Verbosity 2 (подробный):**
```
=== ПЕРЕСЧЕТ КОРНЕВЫХ СЕРИЙ ===
Этап 1: Ищем корневые серии в таблице квартир...
✓ Найдено корневых серий: 241
  ✓ 0008 П-44
  ✓ 0009 П-3
  ✓ 0012 II-49
  ✓ 0017 КОПЭ
  ...

Этап 2: Главная магия - обрабатываем все серии в иерархии...
  0001: Нет корня
  0002: Нет корня
  0006: корень → 12
  0007: корень → 9
  0008: корневая
  ...
```

**Verbosity 3 (очень подробный - таблица):**
```
=== ПЕРЕСЧЕТ КОРНЕВЫХ СЕРИЙ ===
Этап 1: Ищем корневые серии в таблице квартир...
✓ Найдено корневых серий: 241
  ✓ 0008 | П-44  | корневая
  ✓ 0009 | П-3   | корневая
  ✓ 0012 | II-49 | корневая
  ...

Этап 2: Главная магия - обрабатываем все серии в иерархии...
--------------------------------------------------------------------------------------------------------------
   ID | Название                            |   Родитель | Путь                                     | Результат           
--------------------------------------------------------------------------------------------------------------
 ...
 ...
 2565 | Г-ЗИ                                |          9 | 9                                        | ✓ Корень #9
 2566 | I-528КП-809/69                      |          - | (нет)                                    | ✗ Нет корня
 2567 | 464Д-0154                           |       3339 | 3339 → 963 → 375                         | ✓ Корень #375
 2568 | УЛГ-507-4/64                        |       2105 | 2105                                     | ✓ Корень #2105
 2569 | ЛГ-507-4                            |       2105 | 2105                                     | ✓ Корень #2105
 2570 | 1ЛГ-600-И-1                         |          - | (нет)                                    | ✗ Нет корня
 2571 | 464Д-0154 Новополоцкого ДСК         |       3339 | 3339 → 963 → 375                         | ✓ Корень #375
 2572 | 121-0142,13,87                      |          - | (нет)                                    | ✗ Нет корня
 ...
 ...
--------------------------------------------------------------------------------------------------------------

=== РЕЗУЛЬТАТЫ ===
✓ Корневых серий (обработаны на этапе 1): 31
✓ Серий с найденным корнем: 1957
⚠ Серий без корня: 2502

✅ Пересчет завершен! Время: 2.18с
```

### Когда запускать

- **После первого развертывания** — консолидировать иерархию серий.
- **После ручного редактирования иерархии** (добавления родитель-потомков в админку).
- **После загрузки новых данных** с разными орфографическими вариантами серий.
- **По расписанию** (опционально, например раз в месяц):
  ```bash
  0 2 * * 1 cd /home/user/app-path/2022-oknardia && poetry run python oknardia/manage.py regenerate_seria_roots >> /var/log/oknardia-seria-roots.log 2>&1
  ```

### Откат и безопасность

- **Безопасна для повторного запуска** — просто пересчитывает все `kRoot_id`.
- **Откат через SQL** — если нужно очистить поле (перед запуском рекомендуется бэкап):
  ```sql
  UPDATE oknardia_seria_info SET kRoot_id = NULL;
  ```
- **Проверка результатов** — после запуска можно проверить:
  ```bash
  poetry run python oknardia/manage.py shell -c "
  from oknardia.models import Seria_Info
  count_null = Seria_Info.objects.filter(kRoot_id__isnull=True).count()
  count_with_root = Seria_Info.objects.filter(kRoot_id__isnull=False).count()
  print(f'Серий без корня: {count_null}')
  print(f'Серий с корнем: {count_with_root}')
  "
  ```

## 2) Команда `generate_map_js`

Назначение:
- сгенерировать JavaScript-файл для отрисовки карты всех зданий типовых серий в Яндекс.Картах.
- файл содержит геоданные (latitude/longitude), ID адресов, привязку к сериям и информацию для balloon-окон на картах.

### Что происходит

1. **Сбор геоданных** — для всех корневых серий (где `id = kRoot_id`)
   - Запрашиваются здания из таблицы `Building_Info` с non-zero координатами
   - Для каждого здания собирается: широта, долгота, ID адреса, адрес в латинице, ID серии

2. **Генерация JavaScript** — на основе шаблона `service/JavaScript4AllSeriaMap.js.html`
   - Генерируется массив цветов для каждой серии
   - Объявляются переменные с ID и названиями серий
   - Инициализируется Yandex.Maps с PlaceMarks для каждого здания

3. **Минификация через Terser** — уменьшение размера JavaScript
   - Удаляются ненужные пробелы и переносы строк
   - Сокращаются имена переменных (mangling)
   - Удаляются console.log и debugger

4. **Запись в файлы**:
   - `public/static/js/4maps/_ALL_seria_on_map.js` — исходный форматированный файл (715 KB)
   - `public/static/js/4maps/_ALL_seria_on_map.mini.js` — минифицированный файл (639 KB)

### Оптимизация размера

Файл был оптимизирован в три этапа:

| Этап | Размер | Сжатие |
|------|--------|--------|
| Исходный (2016 год) | 2.5 MB | — |
| **Уровень 1**: функция-фабрика `m()` | 715 KB | **71%** |
| **Уровень 2**: Terser минификация | 639 KB | +10.6% |
| **Уровень 3**: Gzip в браузере | 188 KB | +29.4% |
| **Итого сжатие** | **188 KB** | **92.5%** |

> **Примечание**: Gzip применяется автоматически браузером и веб-сервером при наличии в заголовках `Content-Encoding: gzip`

Содержимое:
- **Маркеры на карте**: 18,228 зданий
- **Серии с цветами**: 31  
- **Корневые серии**: 31

### Базовый запуск

```bash
cd /Users/e-serg/PRJ/2022-oknardia
poetry run python oknardia/manage.py generate_map_js
```

### Параметры запуска

**`--force`** — пересгенерировать файл (перезаписать если существует):

```bash
poetry run python oknardia/manage.py generate_map_js --force
```

**`--verbosity 2`** — подробный вывод со статистикой:

```bash
poetry run python oknardia/manage.py generate_map_js --verbosity 2
```

### Когда запускать

- **После первого развертывания** — создать файл карты один раз.
- **После добавления новых зданий** в БД (через парсеры или импорт).
- **По расписанию** (опционально, если здания редко добавляются):
  ```bash
  0 3 * * 0 cd /home/user/app-path/2022-oknardia && poetry run python oknardia/manage.py generate_map_js >> /var/log/oknardia-map-js.log 2>&1
  ```

### Пример вывода

```
=== ГЕНЕРАЦИЯ JAVASCRIPT ДЛЯ КАРТ ===
                                                                                                                                                                                                                                                                                          
Этап 1: Сбор информации о корневых сериях...
✓ Найдено корневых серий: 31

Этап 2: Генерация единого JS-файла для ВСЕ серий...
✓ Написан исходный файл: _ALL_seria_on_map.js
  Размер: 734.0 KB

Этап 3: Минификация JavaScript (rjsmin)...
[*] Минификация успешна!
    Исходный файл: 734.015 KB
    Минифицированный: 732.952 KB
    Сжатие: 0.14%
    Время: 0.0017с
[i] Полная статистика по сериям:
    - Жилых м²: 125,749,341
    - Муниципальных м²: 11,302,860
    - Жильцов: 6,342,742
    - Квартир: 2,769,800

=== РЕЗУЛЬТАТЫ ===                                                                                                                                                                                                                                                                        
✓ Серий обработано: 31
✓ Зданий на карте: 18228
✓ JS-файлов создано: 2 (исходный + минифицированный)
✓ Исходный файл: _ALL_seria_on_map.js
✓ Минифицированный: _ALL_seria_on_map.mini.js
✓ Обфускация: Base64 кодирование координат

[OK] Генерация завершена! Время: 1.10с
```

## 3) Команда `generate_sitemaps`

Назначение:
- пересобрать `sitemap.xml` и chunk-файлы в `MEDIA_ROOT/_serv_sitemap`.

Базовый запуск:

```bash
cd /Users/e-serg/PRJ/2022-oknardia
poetry run python oknardia/manage.py generate_sitemaps
```

Запуск с параметрами:

```bash
cd /Users/e-serg/PRJ/2022-oknardia
poetry run python oknardia/manage.py generate_sitemaps \
  --compare-min-depth 2 \
  --compare-max-depth 4 \
  --max-items 40000 \
  --max-file-size 5242880 \
  --max-files-qty 998
```

Когда запускать:
- после деплоя;
- по расписанию (cron/systemd timer);
- после крупных изменений данных каталога/блога.

### Важные замечания

Чтобы `sitemap.xml` отдавал прокси-nginx напрямую из файловой системы, нужно, чтобы он физически лежал
в `MEDIA_ROOT/_serv_sitemap/sitemap.xml`. 

Допустимо, что файл доступен по двум URL (корневой и media), но в `robots.txt` должен быть указан один
канонический вариант `sitemap.xml`

#### NGINX snippet (alias для корневого sitemap)

```nginx
# Корневой sitemap.xml (для привычного для поисковиков URL)
location = /sitemap.xml {
    alias /<путь-к-каталогку-с-докер-приложением>/media/_serv_sitemap/sitemap.xml;
    default_type application/xml;
    add_header Cache-Control "public, max-age=300";
}
```

## 4) Команда `regenerate_seria_prerender`

Назначение:
- пересобрать pre-render шаблоны для страниц серий (`catalog_seria_info`) в каталоге `seria_info/prepared/`.

Проверка без записи файлов:

```bash
cd /Users/e-serg/PRJ/2022-oknardia
poetry run python oknardia/manage.py regenerate_seria_prerender --dry-run
```

Пересборка только отсутствующих файлов:

```bash
cd /Users/e-serg/PRJ/2022-oknardia
poetry run python oknardia/manage.py regenerate_seria_prerender
```

Принудительная пересборка всех root-серий:

```bash
cd /Users/e-serg/PRJ/2022-oknardia
poetry run python oknardia/manage.py regenerate_seria_prerender --force
```

Выборочная пересборка:

```bash
cd /Users/e-serg/PRJ/2022-oknardia
poetry run python oknardia/manage.py regenerate_seria_prerender --seria-id 843 --seria-id 2100 --force
```

Когда запускать:
- после обновления логики `catalog_seria_info`;
- после массового обновления данных серий/окон/квартир;
- после очистки `seria_info/prepared/`.

## 5) Команда `populate_seo_fields`

Назначение:
- автозаполнить SEO-поля (`sSlug`, `sMetaDescription`, `sMetaKeywords`) для всех существующих записей блога.

Используется:
- при первом развертывании новой версии с автогенерацией SEO-полей;
- при восстановлении из бэкапа где SEO-поля пусты;
- при изменении логики автогенерации (с флагом `--force`).

### Базовый запуск

Заполнить только пустые SEO-поля (стандартный вариант):

```bash
cd /Users/e-serg/PRJ/2022-oknardia
poetry run python oknardia/manage.py populate_seo_fields
```

### Параметры запуска

**`--dry-run`** — только показать что будет сделано (без сохранения в БД):

```bash
poetry run python oknardia/manage.py populate_seo_fields --dry-run
```

**`--force`** — переполнить ВСЕ SEO-поля, даже уже заполненные:

```bash
poetry run python oknardia/manage.py populate_seo_fields --force
```

**`--clean`** — очистить все SEO-поля перед заполнением (для переделки):

```bash
poetry run python oknardia/manage.py populate_seo_fields --clean
```

**Комбинация флагов** — сухой прогон переполнения всех полей:

```bash
poetry run python oknardia/manage.py populate_seo_fields --dry-run --force
```

### Что заполняется

| Поле | Источник | Результат |
|------|----------|-----------|
| `sSlug` | `sPostHeader` | URL-безопасный слаг (max 200 символов) |
| `sMetaDescription` | `sPostContent` | Первые 160 символов (исключая теги `<cut>`) |
| `sMetaKeywords` | `sPostHeader` | Заголовок + префикс "oknardia, окнардия, блог, публикация" (max 256 символов) |

Пример результата:

```python
sPostHeader = "Профиль Brusbox Super Aero"
↓
sSlug = "profil-brusbox-super-aero"
sMetaDescription = "brusbox-super-aero-pyatikamernaya-profil-sistema..."
sMetaKeywords = "oknardia, окнардия, блог, публикация, Профиль Brusbox Super Aero"
```

### Когда запускать

- **После первого развертывания** — заполнить SEO-поля всех 29 существующих постов одной командой.
- **Один раз** — команда идемпотентна (при повторном запуске не будет ничего менять, т.к. пустые поля остатся).
- **При изменении логики** — использовать `--clean --force` для полной переделки всех SEO-полей.

### Пример полного сценария

```bash
cd /Users/e-serg/PRJ/2022-oknardia

# Шаг 1: Проверить что будет заполнено
poetry run python oknardia/manage.py populate_seo_fields --dry-run

# Шаг 2: Если результат устраивает — запустить реально
poetry run python oknardia/manage.py populate_seo_fields

# Шаг 3: Проверить что заполнилось
poetry run python oknardia/manage.py shell -c "
from oknardia.models import BlogPosts
posts = BlogPosts.objects.all()
print(f'Пусто sSlug: {posts.filter(sSlug=\"\").count()}')
print(f'Пусто sMetaDescription: {posts.filter(sMetaDescription=\"\").count()}')
print(f'Пусто sMetaKeywords: {posts.filter(sMetaKeywords=\"\").count()}')
"
```

### Возвращаемая информация

```
======================================================================
ИТОГОВЫЙ ОТЧЕТ
======================================================================

✓ sSlug заполнено:              28 раз
✓ sMetaDescription заполнено:  28 раз
✓ sMetaKeywords заполнено:     28 раз
✓ Записей обновлено в БД:      28
✗ Ошибок при обработке:        0

✅ Обновлено 28 записей успешно!
```

### Откат и безопасность

- ✅ **Безопасна для повторного запуска** — пустые поля не изменяются при повторной работе.
- ✅ **Откат через SQL** — если нужно очистить, используй: `UPDATE oknardia_blogposts SET sSlug='', sMetaDescription='', sMetaKeywords='';`
- ✅ **Всегда используй `--dry-run`** перед первым запуском для проверки.

## 6) Команда `make_rating`

Назначение:
- пересчитать рейтинги оконных профилей, стеклопакетов и наборов услуг используя адаптированный метод Манна-Уитни (Mann-Whitney U Step Rank).
- сохранить результаты в поля `fProfileRating`, `fGlazingRating`, `fSetRating` (0.0 … 5.0 звёзд).
- заполнить JSON-состав рейтинга (детальный разбор по каждому параметру) в поля `sProfileDescription`, `sGlazingDescription`, `sSetDescription`.
- алгоритм рассчитывает три этапа ранжирования: профили → стеклопакеты → наборы (которые зависят от профилей и стеклопакетов).

### Базовый запуск

Пересчитать рейтинги всех профилей и стеклопакетов (стандартный режим):

```bash
cd /Users/e-serg/PRJ/2022-oknardia
poetry run python oknardia/manage.py make_rating
```

### Параметры запуска

**`--verbosity 0`** — минимум информации (только ошибки):
**`--verbosity 1`** — стандартная информация (по умолчанию):
**`--verbosity 3`** — очень подробный вывод (для отладки, для каждого профиля/стеклопакета таблица):

Пример использования с параметром `--verbosity`:

```bash
poetry run python oknardia/manage.py make_rating --verbosity 3 | head -500
```

### АЛГОРИТМ: Метод Манна-Уитни (Mann-Whitney U Step Rank)

Команда использует адаптированный вариант критерия Манна-Уитни для ранжирования параметров качества оконных
предложений и комопнентов (профилей, стеклопакетов, наборов услуг) на основе их технических характеристик
и популярности у поставщиков.

#### Как это работает:

1. **Сортировка объектов** по одному параметру (например, по теплопередаче):
   - Профиль A: 0.60 Ro → ранг = 0.0  
   - Профиль B: 0.60 Ro → ранг = 0.0 (то же значение, ранг не меняется)  
   - Профиль C: 0.80 Ro → ранг = 1.0 (новое значение, добавляем вес параметра)  
   - Профиль D: 0.95 Ro → ранг = 2.0 (ещё новое значение)  

2. **Направление ранжирования** определяется флагом `revers`:
   - `revers=False` — **БОЛЬШЕ = ЛУЧШЕ** (например, теплопередача, звукоизоляция)  
   - `revers=True` — **МЕНЬШЕ = ЛУЧШЕ** (например, высота в проёме для прочности)  

3. **Нормализация рангов** к диапазону 0.0 … 1.0:
   - Профиль A: 0.0 / 2.0 = 0.0  
   - Профиль B: 0.0 / 2.0 = 0.0  
   - Профиль C: 1.0 / 2.0 = 0.5  
   - Профиль D: 2.0 / 2.0 = 1.0  

4. **Суммирование рангов** по всем параметрам:
   - TmpRating = Σ(ранг_параметра × вес_параметра) 

5. **Преобразование в звёзды** (0.0 … 5.0):
   - ТmpRating нормализуется к 0..1  
   - Умножается на 5.0 для получения финального рейтинга  

#### Пример итогового рейтинга профиля:

```
Профиль "Brusbox Super Aero" 
  Теплопередача: 0.60 Ro (ранг 0.9, вес 1.0)
  Звукоизоляция: 33 дБ (ранг 0.8, вес 1.0)
  Высота в проёме: 112 мм (ранг 0.6, вес 0.3)
  Количество камер: 6 шт (ранг 0.7, вес 0.1)
  
  Итого: (0.9×1.0 + 0.8×1.0 + 0.6×0.3 + 0.7×0.1) / 2.3 ≈ 3.8 звёзд ⭐⭐⭐⭐
```

### ПРОФИЛИ: какие параметры учитываются

| № | Параметр             | Поле БД                    | ЛУЧШЕ              | Вес | Описание                                            |
|---|----------------------|----------------------------|--------------------|-----|-----------------------------------------------------|
| 1 | Звукоизоляция        | `fProfileSoundproofing`    | БОЛЬШЕ дБ          | 1.0 | Сопротивление шуму (дБ)                             |
| 2 | Теплопередача        | `fProfileHeatTransf`       | БОЛЬШЕ Ro          | 1.0 | Сопротивление теплопередаче (м²×°C/Вт)              |
| 3 | Высота в проёме      | `iProfileHeight`           | МЕНЬШЕ мм          | 0.3 | Видимая высота в световом проёме (экономия)         |
| 4 | Высота фальца        | `iProfileRabbet`           | БОЛЬШЕ мм          | 0.2 | Глубина фальца для герметизации                     |
| 5 | Толщина стеклопакета | `iProfileGlazingThickness` | БОЛЬШЕ мм          | 0.2 | Максимальная толщина стеклопакета                   |
| 6 | Толщина профиля      | `iProfileThickness`        | БОЛЬШЕ мм          | 0.2 | Монтажная (боковая) ширина профиля                  |
| 7 | Контуры уплотнения   | `fProfileSeals`            | БОЛЬШЕ контуров    | 1.2 | Количество контуров уплотнения                      |
| 8 | Количество камер     | `iProfileCameras`          | БОЛЬШЕ шт          | 0.1 | Число камер в профиле (из рамки + створки)          |
| 9 | Популярность         | `NumOffer`                 | БОЛЬШЕ предложений | 0.1 | Используется ли профиль в коммерческих предложениях |

**Примеры интерпретации:**
- Профиль с рейтингом **5.0 ⭐⭐⭐⭐⭐**: отличная теплопередача + звукоизоляция + много камер + многоконтурные
  уплотнения.
- Профиль с рейтингом **2.0 ⭐⭐**: среднее качество, слабые характеристики.
- Профиль с рейтингом **0.5 ⭐**: слабые характеристики или производить не предоставил данных и их нет в отрытых источниках.

### СТЕКЛОПАКЕТЫ: какие параметры учитываются

| № | Параметр          | Поле БД                     | ЛУЧШЕ        | Вес  | Описание                                                                               |
|---|-------------------|-----------------------------|--------------|------|----------------------------------------------------------------------------------------|
| 1 | Звукоизоляция     | `fGlazingSoundproofing`     | БОЛЬШЕ дБ    | 1.0  | Звукоизоляционный коэффициент (дБ)                                                     |
| 2 | Теплопередача     | `fGlazingHeatTransfer`      | БОЛЬШЕ Ro    | 1.0  | Сопротивление теплопередаче (м²×°C/Вт)                                                 |
| 3 | Светопропускание  | `fGlazingLightTransmission` | БОЛЬШЕ %     | 0.25 | Коэффициент пропускания видимого света (%), отражение света снаружи                    |
| 4 | Солнцепропускание | `fGlazingPassingSun`        | **МЕНЬШЕ %** | 0.15 | Коэффициент солнечного излучения (SHGC) — В России меньше = лучше для охлаждения летом |
| 5 | Толщина           | `iGlazingThickness`         | БОЛЬШЕ мм    | 0.1  | Общая толщина стеклопакета                                                             |
| 6 | Количество камер  | `iGlazingCamerasN`          | БОЛЬШЕ шт    | 0.1  | Число воздушных/аргоновых камер                                                        |

**Особенности стеклопакетов:**
- **Светопропускание** = как много естественного света проходит в помещение (больше = лучше)
- **Солнцепропускание** = как много солнечного тепла/излучения проходит (в России: меньше = лучше, потому что внутри есть отражающее напыление)
- Двухкамерный (с аргоном) почти всегда лучше однокамерного
- Трёхкамерные = премиум для холодного климата

**Примеры интерпретации:**
- **5.0 ⭐⭐⭐⭐⭐**: трёхкамерный с хорошей теплопередачей, звукоизоляцией (обычно с аргоном и напылением).
- **3.0 ⭐⭐⭐**: двухкамерный, среднее качество
- **1.0 ⭐**: однокамерный старого образца или с плохими характеристиками

### НАБОРЫ: какие параметры учитываются

| № | Параметр          | Поле БД                  | ЛУЧШЕ            | Вес | Описание                                          |
|---|-------------------|--------------------------|------------------|-----|---------------------------------------------------|
| 1 | Актуальность      | `dModify`                | МЕНЬШЕ (свежее)  | 0.3 | Дата последнего обновления (timestamp)            |
| 2 | Доставка          | `bSetDelivery`           | ДА (1)           | 0.8 | Включена ли доставка в стоимость                  |
| 3 | Монтаж/демонтаж   | `bSetUninstallInstall`   | ДА (1)           | 1.0 | Включены ли услуги монтажа и демонтажа            |
| 4 | Подоконник        | `sSetSill`               | ДА (1)           | 0.5 | Включен ли подоконник                             |
| 5 | Водоотлив         | `sSetPanes`              | ДА (1)           | 0.8 | Включен ли водоотлив/козырёк                      |
| 6 | Откос             | `sSetSlope`              | ДА (1)           | 0.5 | Включены ли откосы                                |
| 7 | Климат-контроль   | `sSetClimateControl`     | ДА (1)           | 0.3 | Включено ли управление микроклиматом              |
| 8 | Число предложений | `NumOffer`               | БОЛЬШЕ           | 0.2 | Популярность набора (кол-во активных предложений) |
| 9 | Гибкость скидок   | `iDiscountVariantsCount` | БОЛЬШЕ вариантов | 0.5 | Кол-во вариантов скидок из формулы офиса          |
| 10| Размер скидок     | `fDiscountMax`           | БОЛЬШЕ %         | 1.0 | Максимальная скидка из всех вариантов             |

**ВАЖНО: Итоговый рейтинг набора состоит из трёх компонентов:**
- Рейтинг параметров услуг (Актуальность, Доставка, Монтаж, Подоконник и т.д.)
- Рейтинг входящего стеклопакета (ранжируется отдельно)
- Рейтинг входящего профиля (ранжируется отдельно)

Формула итогового рейтинга набора (fSetRating):
```
k1 = нормализованный TmpRating (услуги) * вес услуг
k2 = нормализованный рейтинг стеклопакета * RARING_WEIGHT_GLAZING_IN_SET (обычно 1.5)
k3 = нормализованный рейтинг профиля * RARING_WEIGHT_PVC_PROFILE_IN_SET (обычно 1.5)

fSetRating = k1 + k2 + k3  (итого от 0.0 до 5.0 звёзд)
```

**Примеры интерпретации:**
- **5.0 ⭐⭐⭐⭐⭐**: набор с премиум компонентами (хороший профиль и стеклопакет) + полный пакет услуг (доставка, монтаж, подоконник, откос, климат-контроль) + значительные скидки.
- **3.5 ⭐⭐⭐⭐**: хороший профиль/стеклопакет + базовые услуги (доставка, монтаж) + скромные скидки.
- **2.0 ⭐⭐**: эконом компоненты или слабые услуги (нет доставки, нет откосов).
- **1.0 ⭐**: минимальный пакет или устаревшие предложения (давно не обновлялись).

### Когда запускать

- **После первого развертывания** — заполнить рейтинги всех профилей, стеклопакетов и наборов.
- **После изменения каталога** (добавление нового профиля/стеклопакета/набора).  
- **После уточнения характеристик** (например, поставщик предоставил новые данные).
  ```bash
  poetry run python oknardia/manage.py make_rating
  ```
  
- **По расписанию** (например, ежемесячно, чтобы пересчитать популярность):  
  ```bash
  30 2 * * 1 cd /home/user/app-path/2022-oknardia && poetry run python oknardia/manage.py make_rating >> /var/log/oknardia-rating.log 2>&1
  ```
- **После обновления весов** в `settings.py` (константы `RANK_PVCP_*`, `RANK_GLAZ_*`).

### Откат и безопасность

- **Безопасна для повторного запуска** — пересчитывает все рейтинги заново.
- **Всегда обновляет только рейтинги** — другие данные в таблицах не меняются.
- **Откат через SQL** — если нужно установить нулевые значения (перед запуском рекомендуется бэкап базы):
  ```sql
  -- Очистить рейтинги профилей
  UPDATE oknardia_pvcprofiles SET fProfileRating = 0.0, sProfileDescription = '{}';
  
  -- Очистить рейтинги стеклопакетов
  UPDATE oknardia_glazing SET fGlazingRating = 0.0, sGlazingDescription = '{}';
  
  -- Очистить рейтинги наборов
  UPDATE oknardia_setkit SET fSetRating = 0.0, sSetDescription = '{}';
  ```

### Примеры из реальных данных

Пример вывода `--verbosity 1`:

```
=== НАЧАЛИ ПЕРЕСЧЁТ РЕЙТИНГОВ ===

========================================
[ЭТАП 1]: Пересчёт рейтингов ПРОФИЛЕЙ...
========================================
  ✓ Обнулены рейтинги у 94 профилей
  ✓ Найдено 94 профилей для ранжирования
  ✓ Сохранено 94 профилей с финальными рейтингами

=============================================
[ЭТАП 2]: Пересчёт рейтингов СТЕКЛОПАКЕТОВ...
=============================================
  ✓ Обнулены рейтинги у 97 стеклопакетов
  ✓ Найдено 97 стеклопакетов для ранжирования
  ✓ Сохранено 97 стеклопакетов с финальными рейтингами

================================================
[ЭТАП 3]: Пересчёт рейтингов НАБОРОВ (SetKit)...
================================================
  ✓ Обнулены рейтинги у 27 наборов
  ✓ Найдено 27 наборов для ранжирования
  ✓ Сохранено 27 наборов с финальными рейтингами

[OK!] ПЕРЕСЧЁТ РЕЙТИНГОВ ЗАВЕРШЁН УСПЕШНО!
   • Обновлено профилей: 94
   • Обновлено стеклопакетов: 97
   • Обновлено наборов: 27
```

Пример вывода `--verbosity 3` (наиболее подробный):

```
=== НАЧАЛИ ПЕРЕСЧЁТ РЕЙТИНГОВ ===

========================================
[ЭТАП 1]: Пересчёт рейтингов ПРОФИЛЕЙ...
========================================

  ✓ Обнулены рейтинги у 94 профилей
  ✓ Найдено 94 профилей для ранжирования
  ...
  ...
  ====================================================================================================
  ПРОФИЛЬ: politech W80 (ID: 78)
  ====================================================================================================
  Характеристика                 Значение        Ранг (0..1)     Вклад          
  ----------------------------------------------------------------------------------------------------
  Высота в проёме                120 мм          0.368 *              
  Популярность                   0 предл.        0.000                
  Теплопередача                  0.91 Ro         0.657 ***            
  Толщина профиля                80 мм           0.588 **             
  Толщина стеклопакета           42 мм           0.409 **             
  Уплотнители                    3 контуров      1.000 *****          
  Фальц                          14 мм           0.150                
  Число камер                    12 шт           0.714 ***            
  Шумоизоляция                   44.00 дБ        0.909 ****           
  ----------------------------------------------------------------------------------------------------
  ИТОГО: Рейтинг = 4.94/5.0 ****
  ...
  ...
  ✓ Сохранено 94 профилей с финальными рейтингами

=============================================
[ЭТАП 2]: Пересчёт рейтингов СТЕКЛОПАКЕТОВ...
=============================================

  ✓ Обнулены рейтинги у 97 стеклопакетов
  ✓ Найдено 97 стеклопакетов для ранжирования
  ...
  ...
  ====================================================================================================
  СТЕКЛОПАКЕТ: Однокамерный 5-4, 25 мм (И+аргон) (ID: 60) | Марка:СПО 5М1-Ar16-И4
  ====================================================================================================
  Характеристика                      Значение             Ранг (0..1)     Вклад          
  ----------------------------------------------------------------------------------------------------
  Камеры                              —                    0.000                
  Светопропускание                    74.00%               0.824 ****           
  Солнцепропускание                   58.00%               0.450 **             
  Теплопередача                       0.91 Ro              0.936 ****           
  Толщина                             25 мм                0.400 **             
  Шумоизоляция                        —                    0.429 **             
  ----------------------------------------------------------------------------------------------------
  ИТОГО: Рейтинг = 4.87/5.0 ****
  ...    
  ...    
  ✓ Сохранено 97 стеклопакетов с финальными рейтингами
  
================================================
[ЭТАП 3]: Пересчёт рейтингов НАБОРОВ (SetKit)...
================================================
  ✓ Обнулены рейтинги у 27 наборов
  ✓ Найдено 27 наборов для ранжирования
  ...  
  ...  
  ========================================================================================================================
  НАБОР: Элит (ID: 3)
  ========================================================================================================================
  Параметр                            Значение             Ранг (0..1)     Вклад          
  ------------------------------------------------------------------------------------------------------------------------
  Актуальность                        свежий               0.375 *              
  Водоотлив                           ✓ Да                 1.000 *****          
  Гибкость скидок                     0 вариантов          0.500 **             
  Доставка                            ✓ Да                 1.000 *****          
  Климат-контроль                     ✓ Да                 1.000 *****          
  Монтаж                              ✓ Да                 1.000 *****          
  Откос                               ✓ Да                 1.000 *****          
  Подоконник                          ✓ Да                 1.000 *****          
  Размер скидок                       0.0%                 0.500 **             
  Число предложений                   46 шт                0.250 *              
  ------------------------------------------------------------------------------------------------------------------------
  ИТОГО: Рейтинг = 4.16/5.0 ****
  ...  
  ...  

[OK!] ПЕРЕСЧЁТ РЕЙТИНГОВ ЗАВЕРШЁН УСПЕШНО!
   • Обновлено профилей: 94
   • Обновлено стеклопакетов: 97
   • Обновлено наборов: 27
```

## Оркестрация и reload веб-сервера

Важно:
- reload веб-сервера не встроен в management-команды;
- это отдельная операция окружения.

Пример для systemd + gunicorn:

```bash
sudo systemctl reload gunicorn
```

Рекомендуемый batch-сценарий:

```bash
cd /Users/e-serg/PRJ/2022-oknardia
poetry run python oknardia/manage.py regenerate_seria_prerender --force
poetry run python oknardia/manage.py generate_sitemaps
sudo systemctl reload gunicorn
```

## Cron/systemd timer (пример)

Пример cron (раз в сутки в 03:20):

```bash
20 3 * * * cd /Users/e-serg/PRJ/2022-oknardia && poetry run python oknardia/manage.py regenerate_seria_prerender --force && poetry run python oknardia/manage.py generate_sitemaps >> /var/log/oknardia-maintenance.log 2>&1
```

Если нужен reload после batch, добавляй отдельной строкой/шагом оркестратора.

## Диагностика

Быстрая проверка конфигурации:

```bash
cd /Users/e-serg/PRJ/2022-oknardia
poetry run python oknardia/manage.py check
```

Типовые причины проблем:
- нет прав записи в директории `templates/seria_info/prepared` или `MEDIA_ROOT/_serv_sitemap`;
- устаревшее виртуальное окружение / неустановленные зависимости;
- запуск не из того каталога.

## План миграции `/service/*` -> management commands

Текущее направление:
- все тяжелые и административные операции переносить из HTTP в management-команды;
- `/service/*` оставлять только как thin UI/мониторинг или убрать полностью.



---

См. также:
- `SETUP.md`
- `README.md`

