Compare commits

..

2 Commits

18 changed files with 210 additions and 2358 deletions

View File

@@ -1,249 +0,0 @@
# Чеклист для деплоя в Production
## Перед деплоем
### 1. Безопасность
- [ ] Сгенерирован новый `SECRET_KEY`
```bash
python generate_secret_key.py
```
- [ ] Изменены все пароли в `.env`:
- [ ] `DB_PASSWORD` - сильный пароль для PostgreSQL
- [ ] `POSTGRES_PASSWORD` - должен совпадать с `DB_PASSWORD`
- [ ] Настроен `ALLOWED_HOSTS` в `.env`:
```
ALLOWED_HOSTS=yourdomain.com,www.yourdomain.com
```
- [ ] `DEBUG=False` в `.env`
### 2. База данных
- [ ] Проверены все миграции:
```bash
docker-compose -f docker-compose.prod.yaml exec web python manage.py showmigrations
```
- [ ] Настроен backup БД (cron job):
```bash
0 2 * * * cd /path/to/project && make backup
```
### 3. Статические файлы
- [ ] Проверена директория для статики:
```bash
docker-compose -f docker-compose.prod.yaml exec web python manage.py collectstatic --noinput
```
### 4. SSL/HTTPS (опционально, но рекомендуется)
- [ ] Получены SSL сертификаты (Let's Encrypt, Certbot)
- [ ] Сертификаты размещены в `nginx/ssl/`
- [ ] Переименован `nginx/conf.d/ssl.conf.example` в `ssl.conf`
- [ ] Обновлен `server_name` в `ssl.conf`
### 5. Nginx
- [ ] Проверена конфигурация Nginx:
```bash
docker-compose -f docker-compose.prod.yaml exec nginx nginx -t
```
- [ ] Настроены правильные домены в `nginx/conf.d/default.conf`
### 6. Docker
- [ ] Проверен `.dockerignore` - исключены ненужные файлы
- [ ] Проверен `.gitignore` - не коммитятся секреты
### 7. Переменные окружения
Проверьте `.env` файл:
```bash
# Django
DEBUG=False
ENVIRONMENT=production
DJANGO_SETTINGS_MODULE=dbapp.settings.production
SECRET_KEY=<ваш-длинный-секретный-ключ>
# Database
DB_NAME=geodb
DB_USER=geralt
DB_PASSWORD=<сильный-пароль>
DB_HOST=db
DB_PORT=5432
# Allowed Hosts
ALLOWED_HOSTS=yourdomain.com,www.yourdomain.com
# PostgreSQL
POSTGRES_DB=geodb
POSTGRES_USER=geralt
POSTGRES_PASSWORD=<тот-же-сильный-пароль>
# Gunicorn
GUNICORN_WORKERS=3
GUNICORN_TIMEOUT=120
```
## Деплой
### 1. Клонирование репозитория
```bash
git clone <your-repo-url>
cd <project-directory>
```
### 2. Настройка окружения
```bash
cp .env.prod .env
nano .env # Отредактируйте все необходимые переменные
```
### 3. Запуск контейнеров
```bash
docker-compose -f docker-compose.prod.yaml up -d --build
```
### 4. Проверка статуса
```bash
docker-compose -f docker-compose.prod.yaml ps
docker-compose -f docker-compose.prod.yaml logs -f
```
### 5. Создание суперпользователя
```bash
docker-compose -f docker-compose.prod.yaml exec web python manage.py createsuperuser
```
### 6. Проверка работоспособности
- [ ] Открыть http://yourdomain.com
- [ ] Открыть http://yourdomain.com/admin
- [ ] Проверить статические файлы
- [ ] Проверить медиа файлы
- [ ] Проверить TileServer GL: http://yourdomain.com:8080
## После деплоя
### 1. Мониторинг
- [ ] Настроить мониторинг логов:
```bash
docker-compose -f docker-compose.prod.yaml logs -f web
```
- [ ] Проверить использование ресурсов:
```bash
docker stats
```
### 2. Backup
- [ ] Настроить автоматический backup БД
- [ ] Проверить восстановление из backup
- [ ] Настроить backup медиа файлов
### 3. Обновления
- [ ] Документировать процесс обновления
- [ ] Тестировать обновления на dev окружении
### 4. Безопасность
- [ ] Настроить firewall (UFW, iptables)
- [ ] Ограничить доступ к портам:
- Открыть: 80, 443
- Закрыть: 5432, 8000 (доступ только внутри Docker сети)
- [ ] Настроить fail2ban (опционально)
### 5. Производительность
- [ ] Настроить кэширование (Redis, Memcached)
- [ ] Оптимизировать количество Gunicorn workers
- [ ] Настроить CDN для статики (опционально)
## Troubleshooting
### Проблема: Контейнеры не запускаются
```bash
# Проверить логи
docker-compose -f docker-compose.prod.yaml logs
# Проверить конфигурацию
docker-compose -f docker-compose.prod.yaml config
```
### Проблема: База данных недоступна
```bash
# Проверить статус БД
docker-compose -f docker-compose.prod.yaml exec db pg_isready -U geralt
# Проверить логи БД
docker-compose -f docker-compose.prod.yaml logs db
```
### Проблема: Статические файлы не загружаются
```bash
# Пересобрать статику
docker-compose -f docker-compose.prod.yaml exec web python manage.py collectstatic --noinput
# Проверить права доступа
docker-compose -f docker-compose.prod.yaml exec web ls -la /app/staticfiles
```
### Проблема: 502 Bad Gateway
```bash
# Проверить, что Django запущен
docker-compose -f docker-compose.prod.yaml ps web
# Проверить логи Gunicorn
docker-compose -f docker-compose.prod.yaml logs web
# Проверить конфигурацию Nginx
docker-compose -f docker-compose.prod.yaml exec nginx nginx -t
```
## Полезные команды
```bash
# Перезапуск сервисов
docker-compose -f docker-compose.prod.yaml restart web
docker-compose -f docker-compose.prod.yaml restart nginx
# Обновление кода
git pull
docker-compose -f docker-compose.prod.yaml up -d --build
# Backup БД
docker-compose -f docker-compose.prod.yaml exec db pg_dump -U geralt geodb > backup.sql
# Восстановление БД
docker-compose -f docker-compose.prod.yaml exec -T db psql -U geralt geodb < backup.sql
# Просмотр логов
docker-compose -f docker-compose.prod.yaml logs -f --tail=100 web
# Очистка старых образов
docker system prune -a
```
## Контакты для поддержки
- Документация: [DOCKER_README.md](DOCKER_README.md)
- Быстрый старт: [QUICKSTART.md](QUICKSTART.md)

View File

@@ -1,102 +0,0 @@
# Инструкция по развертыванию изменений
## Шаг 1: Применение миграций
```bash
cd dbapp
python manage.py migrate
```
Это создаст таблицу `lyngsatapp_lyngsat` в базе данных.
## Шаг 2: Запуск FlareSolver (если еще не запущен)
FlareSolver необходим для обхода защиты Cloudflare на сайте Lyngsat.
### Вариант 1: Docker
```bash
docker run -d -p 8191:8191 --name flaresolverr ghcr.io/flaresolverr/flaresolverr:latest
```
### Вариант 2: Docker Compose
Добавьте в `docker-compose.yaml`:
```yaml
services:
flaresolverr:
image: ghcr.io/flaresolverr/flaresolverr:latest
container_name: flaresolverr
ports:
- "8191:8191"
restart: unless-stopped
```
Затем запустите:
```bash
docker-compose up -d flaresolverr
```
## Шаг 3: Проверка работоспособности
1. Запустите сервер разработки:
```bash
python manage.py runserver
```
2. Откройте браузер и перейдите на:
```
http://localhost:8000/actions/
```
3. Найдите карточку "Заполнение данных Lyngsat" и нажмите на кнопку
4. Выберите один-два спутника для тестирования
5. Выберите регионы (например, только Europe)
6. Нажмите "Заполнить данные" и дождитесь завершения
## Шаг 4: Проверка результатов
1. Перейдите в админ-панель Django:
```
http://localhost:8000/admin/
```
2. Откройте раздел "Lyngsatapp" → "Источники LyngSat"
3. Проверьте, что данные загружены корректно
## Возможные проблемы и решения
### Проблема: FlareSolver не отвечает
**Решение**: Проверьте, что FlareSolver запущен:
```bash
curl http://localhost:8191/v1
```
### Проблема: Спутники не найдены в базе
**Решение**: Убедитесь, что спутники добавлены в базу данных. Используйте функцию "Добавление списка спутников" на странице действий.
### Проблема: Долгое выполнение
**Решение**: Это нормально. Процесс может занять несколько минут на спутник. Начните с 1-2 спутников для тестирования.
### Проблема: Ошибки при парсинге
**Решение**: Проверьте логи. Некоторые ошибки (например, некорректные частоты) не критичны и не прерывают процесс.
## Откат изменений (если необходимо)
Если нужно откатить изменения:
```bash
# Откатить миграцию
python manage.py migrate lyngsatapp zero
# Откатить изменения в коде
git checkout HEAD -- dbapp/
```
## Дополнительная информация
- Подробное руководство пользователя: `LYNGSAT_FILL_GUIDE.md`
- Сводка изменений: `CHANGES_SUMMARY.md`
- Документация по проекту: `README.md`

View File

@@ -1,262 +0,0 @@
# Docker Setup для Django + PostGIS + TileServer GL
## Структура проекта
```
.
├── dbapp/ # Django приложение
│ ├── Dockerfile # Универсальный Dockerfile
│ ├── entrypoint.sh # Скрипт запуска
│ └── ...
├── nginx/ # Конфигурация Nginx (только для prod)
│ └── conf.d/
│ └── default.conf
├── tiles/ # Тайлы для TileServer GL
├── docker-compose.yaml # Development окружение
├── docker-compose.prod.yaml # Production окружение
├── .env.dev # Переменные для development
└── .env.prod # Переменные для production
```
## Быстрый старт
### Development
1. Скопируйте файл окружения:
```bash
cp .env.dev .env
```
2. Запустите контейнеры:
```bash
docker-compose up -d --build
```
3. Создайте суперпользователя:
```bash
docker-compose exec web python manage.py createsuperuser
```
4. Приложение доступно:
- Django: http://localhost:8000
- TileServer GL: http://localhost:8080
- PostgreSQL: localhost:5432
### Production
1. Скопируйте и настройте файл окружения:
```bash
cp .env.prod .env
# Отредактируйте .env и измените SECRET_KEY, пароли и ALLOWED_HOSTS
```
2. Запустите контейнеры:
```bash
docker-compose -f docker-compose.prod.yaml up -d --build
```
3. Создайте суперпользователя:
```bash
docker-compose -f docker-compose.prod.yaml exec web python manage.py createsuperuser
```
4. Приложение доступно:
- Nginx: http://localhost (порт 80)
- Django (напрямую): http://localhost:8000
- TileServer GL: http://localhost:8080
- PostgreSQL: localhost:5432
## Основные команды
### Development
```bash
# Запуск
docker-compose up -d
# Остановка
docker-compose down
# Просмотр логов
docker-compose logs -f web
# Выполнение команд Django
docker-compose exec web python manage.py migrate
docker-compose exec web python manage.py createsuperuser
docker-compose exec web python manage.py shell
# Пересборка после изменений в Dockerfile
docker-compose up -d --build
# Полная очистка (включая volumes)
docker-compose down -v
```
### Production
```bash
# Запуск
docker-compose -f docker-compose.prod.yaml up -d
# Остановка
docker-compose -f docker-compose.prod.yaml down
# Просмотр логов
docker-compose -f docker-compose.prod.yaml logs -f web
# Выполнение команд Django
docker-compose -f docker-compose.prod.yaml exec web python manage.py migrate
docker-compose -f docker-compose.prod.yaml exec web python manage.py createsuperuser
# Пересборка
docker-compose -f docker-compose.prod.yaml up -d --build
```
## Различия между Dev и Prod
### Development
- Django development server (runserver)
- DEBUG=True
- Код монтируется как volume (изменения применяются сразу)
- Без Nginx
- Простые пароли (для локальной разработки)
### Production
- Gunicorn WSGI server
- DEBUG=False
- Код копируется в образ (не монтируется)
- Nginx как reverse proxy
- Сильные пароли и SECRET_KEY
- Сбор статики (collectstatic)
- Оптимизированные настройки безопасности
## Переменные окружения
### Основные переменные (.env)
```bash
# Django
DEBUG=True/False
ENVIRONMENT=development/production
DJANGO_SETTINGS_MODULE=dbapp.settings.development/production
SECRET_KEY=your-secret-key
# Database
DB_NAME=geodb
DB_USER=geralt
DB_PASSWORD=your-password
DB_HOST=db
DB_PORT=5432
# Allowed Hosts
ALLOWED_HOSTS=localhost,127.0.0.1,yourdomain.com
# Gunicorn (только для production)
GUNICORN_WORKERS=3
GUNICORN_TIMEOUT=120
```
## Volumes
### Development
- `postgres_data_dev` - данные PostgreSQL
- `static_volume_dev` - статические файлы
- `media_volume_dev` - медиа файлы
- `logs_volume_dev` - логи
- `./dbapp:/app` - код приложения (live reload)
### Production
- `postgres_data_prod` - данные PostgreSQL
- `static_volume_prod` - статические файлы
- `media_volume_prod` - медиа файлы
- `logs_volume_prod` - логи
## TileServer GL
Для работы TileServer GL поместите ваши тайлы в директорию `./tiles/`.
Пример структуры:
```
tiles/
├── config.json
└── your-tiles.mbtiles
```
## Backup и восстановление БД
### Backup
```bash
# Development
docker-compose exec db pg_dump -U geralt geodb > backup.sql
# Production
docker-compose -f docker-compose.prod.yaml exec db pg_dump -U geralt geodb > backup.sql
```
### Восстановление
```bash
# Development
docker-compose exec -T db psql -U geralt geodb < backup.sql
# Production
docker-compose -f docker-compose.prod.yaml exec -T db psql -U geralt geodb < backup.sql
```
## Troubleshooting
### Проблемы с миграциями
```bash
docker-compose exec web python manage.py migrate --fake-initial
```
### Проблемы с правами доступа
```bash
docker-compose exec -u root web chown -R app:app /app
```
### Очистка всех данных
```bash
docker-compose down -v
docker system prune -a
```
### Проверка логов
```bash
# Все сервисы
docker-compose logs -f
# Конкретный сервис
docker-compose logs -f web
docker-compose logs -f db
```
## Безопасность для Production
1. **Измените SECRET_KEY** - используйте длинный случайный ключ
2. **Измените пароли БД** - используйте сильные пароли
3. **Настройте ALLOWED_HOSTS** - укажите ваш домен
4. **Настройте SSL** - добавьте сертификаты в `nginx/ssl/`
5. **Ограничьте доступ к портам** - не открывайте порты БД наружу
## Генерация SECRET_KEY
```python
python -c "from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())"
```
## Мониторинг
### Проверка статуса контейнеров
```bash
docker-compose ps
```
### Использование ресурсов
```bash
docker stats
```
### Healthcheck
```bash
curl http://localhost:8000/admin/
```

View File

@@ -1,307 +0,0 @@
# Docker Setup - Полное руководство
## 📋 Обзор
Этот проект использует Docker для развертывания Django приложения с PostGIS и TileServer GL.
**Основные компоненты:**
- Django 5.2 с PostGIS
- PostgreSQL 17 с расширением PostGIS 3.4
- TileServer GL для работы с картографическими тайлами
- Nginx (только для production)
- Gunicorn WSGI сервер (production)
## 🚀 Быстрый старт
### Development
```bash
cp .env.dev .env
make dev-up
make createsuperuser
```
Откройте http://localhost:8000
### Production
```bash
cp .env.prod .env
# Отредактируйте .env (SECRET_KEY, пароли, домены)
make prod-up
make prod-createsuperuser
```
Откройте http://yourdomain.com
## 📁 Структура файлов
```
.
├── dbapp/ # Django приложение
│ ├── Dockerfile # Универсальный Dockerfile
│ ├── entrypoint.sh # Скрипт инициализации
│ ├── .dockerignore # Исключения для Docker
│ └── ...
├── nginx/ # Nginx конфигурация (prod)
│ ├── conf.d/
│ │ ├── default.conf # HTTP конфигурация
│ │ └── ssl.conf.example # HTTPS конфигурация (пример)
│ └── ssl/ # SSL сертификаты
├── tiles/ # Тайлы для TileServer GL
│ ├── README.md # Инструкция по настройке
│ ├── config.json.example # Пример конфигурации
│ └── .gitignore
├── docker-compose.yaml # Development окружение
├── docker-compose.prod.yaml # Production окружение
├── .env.dev # Переменные для dev
├── .env.prod # Переменные для prod (шаблон)
├── Makefile # Удобные команды
├── generate_secret_key.py # Генератор SECRET_KEY
└── Документация:
├── QUICKSTART.md # Быстрый старт
├── DOCKER_README.md # Подробная документация
├── DEPLOYMENT_CHECKLIST.md # Чеклист для деплоя
└── DOCKER_SETUP.md # Этот файл
```
## 🔧 Конфигурация
### Dockerfile
**Один универсальный Dockerfile** для dev и prod:
- Multi-stage build для оптимизации размера
- Установка GDAL, PostGIS зависимостей
- Использование uv для управления зависимостями
- Non-root пользователь для безопасности
- Healthcheck для мониторинга
### entrypoint.sh
Скрипт автоматически:
- Ждет готовности PostgreSQL
- Выполняет миграции
- Собирает статику (только prod)
- Запускает runserver (dev) или Gunicorn (prod)
Поведение определяется переменной `ENVIRONMENT`:
- `development` → Django development server
- `production` → Gunicorn WSGI server
### docker-compose.yaml (Development)
**Сервисы:**
- `db` - PostgreSQL с PostGIS
- `web` - Django приложение
- `tileserver` - TileServer GL
**Особенности:**
- Код монтируется как volume (live reload)
- DEBUG=True
- Django development server
- Простые пароли для локальной разработки
### docker-compose.prod.yaml (Production)
**Сервисы:**
- `db` - PostgreSQL с PostGIS
- `web` - Django с Gunicorn
- `tileserver` - TileServer GL
- `nginx` - Reverse proxy
**Особенности:**
- Код копируется в образ (не монтируется)
- DEBUG=False
- Gunicorn WSGI server
- Nginx для статики и проксирования
- Сильные пароли из .env
- Сбор статики (collectstatic)
## 🔐 Безопасность
### Для Production обязательно:
1. **Сгенерируйте SECRET_KEY:**
```bash
python generate_secret_key.py
```
2. **Измените пароли БД** в `.env`
3. **Настройте ALLOWED_HOSTS:**
```
ALLOWED_HOSTS=yourdomain.com,www.yourdomain.com
```
4. **Настройте SSL/HTTPS** (рекомендуется):
- Получите сертификаты (Let's Encrypt)
- Поместите в `nginx/ssl/`
- Используйте `nginx/conf.d/ssl.conf.example`
5. **Ограничьте доступ к портам:**
- Открыть: 80, 443
- Закрыть: 5432, 8000
## 📊 Мониторинг
### Логи
```bash
# Development
make dev-logs
# Production
make prod-logs
# Конкретный сервис
docker-compose logs -f web
docker-compose logs -f db
```
### Статус
```bash
make status # Development
make prod-status # Production
docker stats # Использование ресурсов
```
### Healthcheck
```bash
curl http://localhost:8000/admin/
```
## 💾 Backup и восстановление
### Backup
```bash
make backup
# или
docker-compose exec db pg_dump -U geralt geodb > backup_$(date +%Y%m%d).sql
```
### Восстановление
```bash
docker-compose exec -T db psql -U geralt geodb < backup.sql
```
### Автоматический backup (cron)
```bash
# Добавьте в crontab
0 2 * * * cd /path/to/project && make backup
```
## 🔄 Обновление
### Development
```bash
git pull
make dev-build
```
### Production
```bash
git pull
make prod-build
make prod-migrate
```
## 🗺️ TileServer GL
Поместите `.mbtiles` файлы в директорию `tiles/`:
```bash
tiles/
├── world.mbtiles
└── satellite.mbtiles
```
Доступ: http://localhost:8080
Подробнее: [tiles/README.md](tiles/README.md)
## 🛠️ Makefile команды
### Development
```bash
make dev-up # Запустить
make dev-down # Остановить
make dev-build # Пересобрать
make dev-logs # Логи
make dev-restart # Перезапустить web
```
### Production
```bash
make prod-up # Запустить
make prod-down # Остановить
make prod-build # Пересобрать
make prod-logs # Логи
make prod-restart # Перезапустить web
```
### Django
```bash
make shell # Django shell
make migrate # Миграции
make makemigrations # Создать миграции
make createsuperuser # Создать суперпользователя
make collectstatic # Собрать статику
```
### Утилиты
```bash
make backup # Backup БД
make status # Статус контейнеров
make clean # Очистка (с volumes)
make clean-all # Полная очистка
```
## 📚 Дополнительная документация
- **[QUICKSTART.md](QUICKSTART.md)** - Быстрый старт для нетерпеливых
- **[DOCKER_README.md](DOCKER_README.md)** - Подробная документация по Docker
- **[DEPLOYMENT_CHECKLIST.md](DEPLOYMENT_CHECKLIST.md)** - Чеклист для деплоя
- **[tiles/README.md](tiles/README.md)** - Настройка TileServer GL
## ❓ Troubleshooting
### Контейнеры не запускаются
```bash
docker-compose logs
docker-compose config
```
### База данных недоступна
```bash
docker-compose exec db pg_isready -U geralt
docker-compose logs db
```
### Статические файлы не загружаются
```bash
docker-compose exec web python manage.py collectstatic --noinput
docker-compose exec web ls -la /app/staticfiles
```
### 502 Bad Gateway
```bash
docker-compose ps web
docker-compose logs web
docker-compose exec nginx nginx -t
```
## 🎯 Следующие шаги
1. ✅ Прочитайте [QUICKSTART.md](QUICKSTART.md)
2. ✅ Запустите development окружение
3. ✅ Изучите [DEPLOYMENT_CHECKLIST.md](DEPLOYMENT_CHECKLIST.md) перед деплоем
4. ✅ Настройте TileServer GL ([tiles/README.md](tiles/README.md))
5. ✅ Настройте SSL для production
## 📞 Поддержка
При возникновении проблем:
1. Проверьте логи: `make dev-logs` или `make prod-logs`
2. Изучите документацию в этой директории
3. Проверьте [DOCKER_README.md](DOCKER_README.md) для подробностей

View File

@@ -1,240 +0,0 @@
# Обзор созданных файлов Docker Setup
## 🐳 Docker файлы
### `dbapp/Dockerfile`
**Универсальный Dockerfile** для dev и prod окружений.
- Multi-stage build для оптимизации
- Установка GDAL, PostGIS, PostgreSQL клиента
- Использование uv для управления зависимостями
- Non-root пользователь для безопасности
- Healthcheck для мониторинга
### `dbapp/entrypoint.sh`
**Скрипт инициализации контейнера.**
- Ожидание готовности PostgreSQL
- Автоматические миграции
- Сбор статики (только prod)
- Запуск runserver (dev) или Gunicorn (prod)
### `dbapp/.dockerignore`
**Исключения для Docker build.**
- Исключает ненужные файлы из образа
- Уменьшает размер образа
- Ускоряет сборку
## 🔧 Docker Compose файлы
### `docker-compose.yaml`
**Development окружение.**
- PostgreSQL с PostGIS
- Django с development server
- TileServer GL
- Код монтируется как volume (live reload)
- DEBUG=True
### `docker-compose.prod.yaml`
**Production окружение.**
- PostgreSQL с PostGIS
- Django с Gunicorn
- TileServer GL
- Nginx reverse proxy
- Код копируется в образ
- DEBUG=False
- Оптимизированные настройки
## 🌐 Nginx конфигурация
### `nginx/conf.d/default.conf`
**HTTP конфигурация для production.**
- Проксирование к Django
- Раздача статики и медиа
- Оптимизированные таймауты
- Кэширование статики
### `nginx/conf.d/ssl.conf.example`
**HTTPS конфигурация (пример).**
- SSL/TLS настройки
- Редирект с HTTP на HTTPS
- Security headers
- Оптимизированные SSL параметры
### `nginx/ssl/.gitkeep`
**Директория для SSL сертификатов.**
- Поместите сюда fullchain.pem и privkey.pem
## 🗺️ TileServer GL
### `tiles/README.md`
**Инструкция по настройке TileServer GL.**
- Как добавить тайлы
- Примеры конфигурации
- Использование в Django/Leaflet
- Где взять тайлы
### `tiles/config.json.example`
**Пример конфигурации TileServer GL.**
- Настройки путей
- Форматы и качество
- Домены
### `tiles/.gitignore`
**Исключения для git.**
- Игнорирует большие .mbtiles файлы
- Сохраняет примеры конфигурации
## 🔐 Переменные окружения
### `.env.dev`
**Переменные для development.**
- DEBUG=True
- Простые пароли для локальной разработки
- Настройки БД для dev
### `.env.prod`
**Шаблон переменных для production.**
- DEBUG=False
- Требует изменения SECRET_KEY и паролей
- Настройки для production
## 🛠️ Утилиты
### `Makefile`
**Удобные команды для работы с Docker.**
- `make dev-up` - запуск dev
- `make prod-up` - запуск prod
- `make migrate` - миграции
- `make backup` - backup БД
- И многое другое
### `generate_secret_key.py`
**Генератор Django SECRET_KEY.**
```bash
python generate_secret_key.py
```
## 📚 Документация
### `QUICKSTART.md`
**Быстрый старт.**
- Минимальные команды для запуска
- Development и Production
- Основные команды
### `DOCKER_README.md`
**Подробная документация.**
- Полное описание структуры
- Все команды с примерами
- Troubleshooting
- Backup и восстановление
### `DOCKER_SETUP.md`
**Полное руководство.**
- Обзор всей системы
- Конфигурация
- Безопасность
- Мониторинг
### `DEPLOYMENT_CHECKLIST.md`
**Чеклист для деплоя.**
- Пошаговая инструкция
- Проверка безопасности
- Настройка production
- Troubleshooting
### `FILES_OVERVIEW.md`
**Этот файл.**
- Описание всех созданных файлов
- Назначение каждого файла
## 📝 Обновленные файлы
### `.gitignore`
**Обновлен для Docker.**
- Исключает .env файлы
- Исключает логи и backup
- Исключает временные файлы
## 🎯 Как использовать
### Для начала работы:
1. Прочитайте **QUICKSTART.md**
2. Выберите окружение (dev или prod)
3. Скопируйте соответствующий .env файл
4. Запустите с помощью Makefile
### Для деплоя:
1. Прочитайте **DEPLOYMENT_CHECKLIST.md**
2. Следуйте чеклисту пошагово
3. Используйте **DOCKER_README.md** для справки
### Для настройки TileServer:
1. Прочитайте **tiles/README.md**
2. Добавьте .mbtiles файлы
3. Настройте config.json (опционально)
## 📊 Структура проекта
```
.
├── Docker конфигурация
│ ├── dbapp/Dockerfile
│ ├── dbapp/entrypoint.sh
│ ├── dbapp/.dockerignore
│ ├── docker-compose.yaml
│ └── docker-compose.prod.yaml
├── Nginx
│ ├── nginx/conf.d/default.conf
│ ├── nginx/conf.d/ssl.conf.example
│ └── nginx/ssl/.gitkeep
├── TileServer GL
│ ├── tiles/README.md
│ ├── tiles/config.json.example
│ └── tiles/.gitignore
├── Переменные окружения
│ ├── .env.dev
│ └── .env.prod
├── Утилиты
│ ├── Makefile
│ └── generate_secret_key.py
└── Документация
├── QUICKSTART.md
├── DOCKER_README.md
├── DOCKER_SETUP.md
├── DEPLOYMENT_CHECKLIST.md
└── FILES_OVERVIEW.md
```
## ✅ Что было сделано
1. ✅ Создан универсальный Dockerfile (один для dev и prod)
2. ✅ Настроен entrypoint.sh с автоматической инициализацией
3. ✅ Созданы docker-compose.yaml для dev и prod
4. ✅ Настроен Nginx для production
5. ✅ Добавлена поддержка TileServer GL
6. ✅ Созданы .env файлы для разных окружений
7. ✅ Добавлен Makefile с удобными командами
8. ✅ Написана подробная документация
9. ✅ Создан чеклист для деплоя
10. ✅ Добавлены утилиты (генератор SECRET_KEY)
## 🚀 Следующие шаги
1. Запустите development окружение
2. Протестируйте все функции
3. Подготовьте production окружение
4. Следуйте DEPLOYMENT_CHECKLIST.md
5. Настройте мониторинг и backup
## 💡 Полезные ссылки
- Django Documentation: https://docs.djangoproject.com/
- Docker Documentation: https://docs.docker.com/
- PostGIS Documentation: https://postgis.net/documentation/
- TileServer GL: https://github.com/maptiler/tileserver-gl
- Nginx Documentation: https://nginx.org/en/docs/

View File

@@ -1,167 +0,0 @@
### Шаг 2: Применение миграций
```bash
cd dbapp
python manage.py migrate
```
Это создаст:
- Таблицу `lyngsatapp_lyngsat` для данных Lyngsat
- Таблицы `django_celery_results_*` для результатов Celery
### Шаг 3: Запуск сервисов
```bash
# Запуск Redis и FlareSolver
docker-compose up -d redis flaresolverr
# Проверка
redis-cli ping # Должно вернуть PONG
curl http://localhost:8191/v1 # Должно вернуть JSON
```
### Шаг 4: Запуск приложения
**Терминал 1 - Django:**
```bash
cd dbapp
python manage.py runserver
```
**Терминал 2 - Celery Worker:**
```bash
cd dbapp
celery -A dbapp worker --loglevel=info
```
### Шаг 5: Тестирование
1. Откройте `http://localhost:8000/actions/`
2. Нажмите "Заполнить данные Lyngsat"
3. Выберите спутники и регионы
4. Наблюдайте за прогрессом!
---
### Проверка Django
```bash
python dbapp/manage.py check
# Должно вывести: System check identified no issues (0 silenced).
```
### Проверка Celery (если установлен)
```bash
celery -A dbapp inspect ping
# Должно вывести: pong
```
### Проверка Redis (если установлен)
```bash
redis-cli ping
# Должно вывести: PONG
```
### Проверка FlareSolver
```bash
curl http://localhost:8191/v1
# Должно вернуть JSON с информацией о сервисе
```
---
### Проблема: FlareSolver не отвечает
**Решение**: Запустите FlareSolver
```bash
docker-compose up -d flaresolverr
# или
docker run -d -p 8191:8191 ghcr.io/flaresolverr/flaresolverr:latest
```
## Дополнительные инструменты
### Flower - мониторинг Celery
```bash
pip install flower
celery -A dbapp flower
# Откройте http://localhost:5555
```
### Redis Commander - GUI для Redis
```bash
docker run -d -p 8081:8081 --name redis-commander \
--env REDIS_HOSTS=local:localhost:6379 \
rediscommander/redis-commander
# Откройте http://localhost:8081
```
### pgAdmin - GUI для PostgreSQL
```bash
docker run -d -p 5050:80 --name pgadmin \
-e PGADMIN_DEFAULT_EMAIL=admin@admin.com \
-e PGADMIN_DEFAULT_PASSWORD=admin \
dpage/pgadmin4
# Откройте http://localhost:5050
```
---
### Остановка сервисов
```bash
# Остановка Docker контейнеров
docker-compose down
# Остановка Celery Worker
pkill -f "celery worker"
```
### Удаление данных
```bash
# Удаление Docker volumes
docker-compose down -v
# Удаление виртуального окружения
rm -rf dbapp/.venv
# Удаление миграций (опционально)
find dbapp -path "*/migrations/*.py" -not -name "__init__.py" -delete
find dbapp -path "*/migrations/*.pyc" -delete
```
# Systemd service для запуска с хоста
[Unit]
Description=Django Application
After=network.target
[Service]
Type=notify
User=www-data
Group=www-data
WorkingDirectory=/path/to/your/app
Environment=PATH=/path/to/venv/bin
Environment=DATABASE_URL=postgresql://user:pass@localhost/geodb
ExecStart=/path/to/venv/bin/python manage.py runserver 0.0.0.0:8000
ExecReload=/bin/kill -s HUP $MAINPID
TimeoutSec=300
Restart=on-failure
[Install]
WantedBy=multi-user.target
## Поддержка
1. Проверьте логи:
- Django: консоль где запущен runserver
- Celery: `dbapp/logs/celery_worker.log`
- Docker: `docker-compose logs`

View File

@@ -1,126 +0,0 @@
# ObjItemListView Query Optimization Report
## Дата: 2025-11-18
## Проблема
При загрузке страницы списка ObjItems с большой пагинацией (500-1000 элементов) возникало **292+ дублирующихся SQL запросов** для получения mirrors (зеркал) через отношение ManyToMany:
```sql
SELECT ••• FROM "mainapp_satellite"
INNER JOIN "mainapp_geo_mirrors" ON ("mainapp_satellite"."id" = "mainapp_geo_mirrors"."satellite_id")
WHERE "mainapp_geo_mirrors"."geo_id" = 4509
ORDER BY 1 ASC
```
Это классическая проблема N+1 запросов, где для каждого ObjItem выполнялся отдельный запрос для получения связанных mirrors.
## Решение
### 1. Добавлен импорт Prefetch
```python
from django.db.models import F, Prefetch
```
### 2. Создан оптимизированный Prefetch для mirrors
```python
mirrors_prefetch = Prefetch(
'geo_obj__mirrors',
queryset=Satellite.objects.only('id', 'name').order_by('id')
)
```
### 3. Применен Prefetch в обоих ветках queryset
Для случая с выбранными спутниками:
```python
objects = (
ObjItem.objects.select_related(
"geo_obj",
"source",
"updated_by__user",
"created_by__user",
"lyngsat_source",
"parameter_obj",
"parameter_obj__id_satellite",
"parameter_obj__polarization",
"parameter_obj__modulation",
"parameter_obj__standard",
"transponder",
"transponder__sat_id",
"transponder__polarization",
)
.prefetch_related(
"parameter_obj__sigma_parameter",
"parameter_obj__sigma_parameter__polarization",
mirrors_prefetch, # ← Оптимизированный prefetch
)
.filter(parameter_obj__id_satellite_id__in=selected_satellites)
)
```
### 4. Добавлены select_related для transponder
Также добавлены оптимизации для transponder, которые ранее отсутствовали:
- `"transponder"`
- `"transponder__sat_id"`
- `"transponder__polarization"`
## Результаты
### До оптимизации
- **50 элементов**: ~295 запросов
- **100 элементов**: ~295 запросов
- **500 элементов**: ~295 запросов
- **1000 элементов**: ~295 запросов
### После оптимизации
- **50 элементов**: **3 запроса**
- **100 элементов**: **3 запроса**
- **500 элементов**: **3 запроса**
- **1000 элементов**: **3 запроса**
### Улучшение производительности
| Метрика | До | После | Улучшение |
|---------|-----|-------|-----------|
| Запросов на 50 элементов | ~295 | 3 | **98.9%** ↓ |
| Запросов на 1000 элементов | ~295 | 3 | **98.9%** ↓ |
| Запросов на элемент | ~5.9 | 0.003 | **99.9%** ↓ |
## Структура запросов после оптимизации
1. **Основной запрос** - получение всех ObjItems с JOIN для всех select_related отношений
2. **Prefetch для sigma_parameter** - один запрос для всех sigma параметров
3. **Prefetch для mirrors** - один запрос для всех mirrors через geo_obj
## Тестирование
Созданы тестовые скрипты для проверки оптимизации:
1. `test_objitem_query_optimization.py` - базовый тест
2. `test_objitem_detailed_queries.py` - детальный тест с доступом ко всем данным
3. `test_objitem_scale.py` - тест масштабируемости (50, 100, 500, 1000 элементов)
Все тесты подтверждают, что количество запросов остается константным (3 запроса) независимо от размера страницы.
## Соответствие требованиям
Задача 29 из `.kiro/specs/django-refactoring/tasks.md`:
- ✅ Добавлен select_related() для всех связанных моделей
- ✅ Добавлен prefetch_related() для mirrors (через Prefetch объект)
- ✅ Проверено количество запросов до и после оптимизации
- ✅ Требования 8.1, 8.2, 8.3, 8.4, 8.6 выполнены
## Дополнительные улучшения
1. Использован `Prefetch` объект вместо простой строки для более точного контроля
2. Добавлен `.only('id', 'name')` для mirrors, чтобы загружать только необходимые поля
3. Добавлен `.order_by('id')` для стабильного порядка результатов
## Заключение
Оптимизация успешно устранила проблему N+1 запросов для mirrors. Количество SQL запросов сокращено с ~295 до 3 (сокращение на **98.9%**), что значительно улучшает производительность страницы, особенно при больших размерах пагинации.

View File

@@ -1,192 +0,0 @@
# SQL Query Optimization Report: SourceListView
## Summary
Successfully optimized SQL queries in `SourceListView` to eliminate N+1 query problems and improve performance.
## Optimization Results
### Query Count
- **Total queries**: 22 (constant regardless of page size)
- **Variation across page sizes**: 0 (perfectly stable)
- **Status**: ✅ EXCELLENT
### Test Results
| Page Size | Query Count | Status |
|-----------|-------------|--------|
| 10 items | 22 queries | ✅ Stable |
| 50 items | 22 queries | ✅ Stable |
| 100 items | 22 queries | ✅ Stable |
**Key Achievement**: Query count remains constant at 22 regardless of the number of items displayed, proving there are no N+1 query problems.
## Optimizations Applied
### 1. select_related() for ForeignKey/OneToOne Relationships
Added `select_related()` to fetch related objects in a single query using SQL JOINs:
```python
sources = Source.objects.select_related(
'info', # ForeignKey to ObjectInfo
'created_by', # ForeignKey to CustomUser
'created_by__user', # OneToOne to User (through CustomUser)
'updated_by', # ForeignKey to CustomUser
'updated_by__user', # OneToOne to User (through CustomUser)
)
```
**Impact**: Eliminates separate queries for each Source's info, created_by, and updated_by relationships.
### 2. prefetch_related() for Reverse ForeignKey and ManyToMany
Added comprehensive `prefetch_related()` to fetch related collections efficiently:
```python
.prefetch_related(
# ObjItems and their nested relationships
'source_objitems',
'source_objitems__parameter_obj',
'source_objitems__parameter_obj__id_satellite',
'source_objitems__parameter_obj__polarization',
'source_objitems__parameter_obj__modulation',
'source_objitems__parameter_obj__standard',
'source_objitems__geo_obj',
'source_objitems__geo_obj__mirrors', # ManyToMany
'source_objitems__lyngsat_source',
'source_objitems__lyngsat_source__satellite',
'source_objitems__transponder',
'source_objitems__created_by',
'source_objitems__created_by__user',
'source_objitems__updated_by',
'source_objitems__updated_by__user',
# Marks and their relationships
'marks',
'marks__created_by',
'marks__created_by__user'
)
```
**Impact**: Fetches all related ObjItems, Parameters, Geo objects, Marks, and their nested relationships in separate optimized queries instead of one query per item.
### 3. annotate() for Efficient Counting
Used `annotate()` with `Count()` to calculate objitem counts in the database:
```python
.annotate(
objitem_count=Count('source_objitems', filter=objitem_filter_q, distinct=True)
if has_objitem_filter
else Count('source_objitems')
)
```
**Impact**: Counts are calculated in the database using GROUP BY instead of Python loops, and the count is available as an attribute on each Source object.
## Query Breakdown
The 22 queries consist of:
1. **1 COUNT query**: For pagination (total count)
2. **1 Main SELECT**: Source objects with JOINs for select_related fields
3. **~20 Prefetch queries**: For all prefetch_related relationships
- ObjItems
- Parameters
- Satellites
- Polarizations
- Modulations
- Standards
- Geo objects
- Mirrors (ManyToMany)
- Transponders
- LyngsatSources
- CustomUsers
- Auth Users
- ObjectMarks
## Performance Characteristics
### Before Optimization (Estimated)
Without proper optimization, the query count would scale linearly with the number of items:
- 10 items: ~100+ queries (N+1 problem)
- 50 items: ~500+ queries
- 100 items: ~1000+ queries
### After Optimization
- 10 items: 22 queries ✅
- 50 items: 22 queries ✅
- 100 items: 22 queries ✅
**Improvement**: ~95-98% reduction in query count for larger page sizes.
## Compliance with Requirements
### Requirement 8.1: Minimize SQL queries
**ACHIEVED**: Query count reduced to 22 constant queries
### Requirement 8.2: Use select_related() for ForeignKey/OneToOne
**ACHIEVED**: Applied to info, created_by, updated_by relationships
### Requirement 8.3: Use prefetch_related() for ManyToMany and reverse ForeignKey
**ACHIEVED**: Applied to all reverse relationships and ManyToMany (mirrors)
### Requirement 8.4: Use annotate() for aggregations
**ACHIEVED**: Used for objitem_count calculation
### Requirement 8.6: Reduce query count by at least 50%
**EXCEEDED**: Achieved 95-98% reduction for typical page sizes
## Testing Methodology
Three test scripts were created to verify the optimization:
1. **test_source_query_optimization.py**: Basic query count test
2. **test_source_query_detailed.py**: Detailed query analysis
3. **test_source_query_scale.py**: Scaling test with different page sizes
All tests confirm:
- No N+1 query problems
- Stable query count across different page sizes
- Efficient use of Django ORM optimization techniques
## Recommendations
1. ✅ The optimization is complete and working correctly
2. ✅ Query count is well within acceptable limits (≤50)
3. ✅ No further optimization needed for SourceListView
4. 📝 Apply similar patterns to other list views (ObjItemListView, TransponderListView, etc.)
## Bug Fix
### Issue
Initial implementation had an incorrect prefetch path:
-`'source_objitems__lyngsat_source__satellite'`
### Resolution
Fixed to use the correct field name from LyngSat model:
-`'source_objitems__lyngsat_source__id_satellite'`
The LyngSat model uses `id_satellite` as the ForeignKey field name, not `satellite`.
### Verification
Tested with 1000 items per page - no errors, 24 queries total.
## Files Modified
- `dbapp/mainapp/views/source.py`: Updated SourceListView.get() method with optimized queryset
## Test Files Created
- `test_source_query_optimization.py`: Basic optimization test
- `test_source_query_detailed.py`: Detailed query analysis
- `test_source_query_scale.py`: Scaling verification test
- `test_source_1000_items.py`: Large page size test (1000 items)
- `OPTIMIZATION_REPORT_SourceListView.md`: This report
---
**Date**: 2025-11-18
**Status**: ✅ COMPLETE (Bug Fixed)
**Task**: 28. Оптимизировать запросы в SourceListView

View File

@@ -1,90 +0,0 @@
# Отчет об оптимизации запросов в ObjItemListView
## Задача 29: Оптимизировать запросы в ObjItemListView
### Выполненные изменения
#### 1. Добавлены select_related() для всех связанных моделей
Добавлены следующие связи через `select_related()`:
- `transponder`
- `transponder__sat_id`
- `transponder__polarization`
Эти связи уже были частично оптимизированы, но были добавлены недостающие.
#### 2. Добавлены prefetch_related() для mirrors и marks
Использованы оптимизированные `Prefetch` объекты:
```python
# Оптимизированный prefetch для mirrors через geo_obj
mirrors_prefetch = Prefetch(
'geo_obj__mirrors',
queryset=Satellite.objects.only('id', 'name').order_by('id')
)
# Оптимизированный prefetch для marks через source
marks_prefetch = Prefetch(
'source__marks',
queryset=ObjectMark.objects.select_related('created_by__user').order_by('-timestamp')
)
```
#### 3. Исправлен доступ к mirrors
Изменен способ доступа к mirrors с `values_list()` на list comprehension:
**Было:**
```python
mirrors_list = list(obj.geo_obj.mirrors.values_list('name', flat=True))
```
**Стало:**
```python
mirrors_list = [mirror.name for mirror in obj.geo_obj.mirrors.all()]
```
Это критически важно, так как `values_list()` обходит prefetch_related и вызывает дополнительные запросы.
### Результаты тестирования
#### Тест 1: Сравнение с baseline (50 объектов)
- **До оптимизации:** 51 запрос
- **После оптимизации:** 4 запроса
- **Улучшение:** 92.2% (сокращение на 47 запросов)
#### Тест 2: Масштабируемость
| Количество объектов | Запросов |
|---------------------|----------|
| 10 | 4 |
| 50 | 4 |
| 100 | 4 |
| 200 | 4 |
**Результат:** ✓ PERFECT! Количество запросов остается постоянным независимо от количества объектов.
### Структура запросов после оптимизации
1. **Основной запрос:** SELECT для ObjItem с JOIN для всех select_related связей
2. **Prefetch mirrors:** SELECT для Satellite через geo_mirrors (ManyToMany)
3. **Prefetch source:** SELECT для Source (если не покрыто select_related)
4. **Prefetch marks:** SELECT для ObjectMark через source
### Требования
Выполнены все требования задачи:
- ✓ 8.1 - Добавлен select_related() для всех связанных моделей
- ✓ 8.2 - Добавлен prefetch_related() для mirrors
- ✓ 8.3 - Добавлен prefetch_related() для marks
- ✓ 8.4 - Проверено количество запросов до и после оптимизации
- ✓ 8.6 - Оптимизация работает корректно
### Файлы изменены
- `dbapp/mainapp/views/objitem.py` - добавлены оптимизации запросов
### Тестовые файлы
- `test_objitem_final.py` - тест сравнения с baseline
- `test_objitem_scale.py` - тест масштабируемости
- `test_objitem_query_optimization.py` - базовый тест
- `test_objitem_detailed_queries.py` - детальный тест
## Заключение
Оптимизация успешно выполнена. Количество запросов к базе данных сокращено с ~51 до 4 запросов (улучшение на 92.2%), и это количество остается постоянным независимо от количества отображаемых объектов. Это значительно улучшит производительность страницы списка объектов, особенно при большом количестве записей.

View File

@@ -1,106 +0,0 @@
# Быстрый старт с Docker
## Development (разработка)
```bash
# 1. Скопировать переменные окружения
cp .env.dev .env
# 2. Запустить контейнеры
make dev-up
# или
docker-compose up -d --build
# 3. Создать суперпользователя
make createsuperuser
# или
docker-compose exec web python manage.py createsuperuser
# 4. Открыть в браузере
# Django: http://localhost:8000
# Admin: http://localhost:8000/admin
# TileServer: http://localhost:8080
```
## Production (продакшн)
```bash
# 1. Скопировать и настроить переменные
cp .env.prod .env
nano .env # Измените SECRET_KEY, пароли, ALLOWED_HOSTS
# 2. Запустить контейнеры
make prod-up
# или
docker-compose -f docker-compose.prod.yaml up -d --build
# 3. Создать суперпользователя
make prod-createsuperuser
# или
docker-compose -f docker-compose.prod.yaml exec web python manage.py createsuperuser
# 4. Открыть в браузере
# Nginx: http://localhost
# Django: http://localhost:8000
# TileServer: http://localhost:8080
```
## Полезные команды
```bash
# Просмотр логов
make dev-logs # development
make prod-logs # production
# Остановка
make dev-down # development
make prod-down # production
# Перезапуск после изменений
make dev-build # development
make prod-build # production
# Django shell
make shell # development
make prod-shell # production
# Миграции
make migrate # development
make prod-migrate # production
# Backup БД
make backup
# Статус контейнеров
make status # development
make prod-status # production
```
## Структура проекта
```
.
├── dbapp/ # Django приложение
│ ├── Dockerfile # Универсальный Dockerfile
│ ├── entrypoint.sh # Скрипт запуска
│ ├── manage.py
│ └── ...
├── nginx/ # Nginx (только prod)
│ └── conf.d/
│ └── default.conf
├── tiles/ # Тайлы для TileServer GL
│ ├── README.md
│ └── config.json.example
├── docker-compose.yaml # Development
├── docker-compose.prod.yaml # Production
├── .env.dev # Переменные dev
├── .env.prod # Переменные prod
├── Makefile # Команды для удобства
└── DOCKER_README.md # Подробная документация
```
## Что дальше?
1. Прочитайте [DOCKER_README.md](DOCKER_README.md) для подробной информации
2. Настройте TileServer GL - см. [tiles/README.md](tiles/README.md)
3. Для production настройте SSL сертификаты в `nginx/ssl/`

View File

@@ -1,117 +0,0 @@
# Быстрый старт: Асинхронное заполнение данных Lyngsat
## Минимальная настройка (5 минут)
### 1. Установите зависимости
```bash
pip install -r dbapp/requirements.txt
```
### 2. Примените миграции
```bash
cd dbapp
python manage.py migrate
```
### 3. Запустите необходимые сервисы
**Терминал 1 - Redis и FlareSolver:**
```bash
docker-compose up -d redis flaresolverr
```
**Терминал 2 - Django:**
```bash
cd dbapp
python manage.py runserver
```
**Терминал 3 - Celery Worker:**
```bash
cd dbapp
celery -A dbapp worker --loglevel=info
```
### 4. Используйте систему
1. Откройте браузер: `http://localhost:8000/actions/`
2. Нажмите "Заполнить данные Lyngsat"
3. Выберите 1-2 спутника для теста
4. Выберите регион (например, Europe)
5. Нажмите "Заполнить данные"
6. Наблюдайте за прогрессом в реальном времени!
## Проверка работоспособности
### Redis
```bash
redis-cli ping
# Должно вернуть: PONG
```
### FlareSolver
```bash
curl http://localhost:8191/v1
# Должно вернуть JSON с информацией о сервисе
```
### Celery Worker
Проверьте вывод в терминале 3 - должны быть сообщения:
```
[2024-01-15 10:30:00,000: INFO/MainProcess] Connected to redis://localhost:6379/0
[2024-01-15 10:30:00,000: INFO/MainProcess] celery@hostname ready.
```
## Остановка сервисов
```bash
# Остановить Docker контейнеры
docker-compose down
# Остановить Django (Ctrl+C в терминале 2)
# Остановить Celery Worker (Ctrl+C в терминале 3)
```
## Просмотр логов
```bash
# Логи Celery Worker (если запущен с --logfile)
tail -f dbapp/logs/celery_worker.log
# Логи Docker контейнеров
docker-compose logs -f redis
docker-compose logs -f flaresolverr
```
## Что дальше?
- Прочитайте полную документацию: `ASYNC_LYNGSAT_GUIDE.md`
- Настройте production окружение
- Добавьте периодические задачи
- Настройте email уведомления
## Решение проблем
**Worker не запускается:**
```bash
# Проверьте Redis
redis-cli ping
# Проверьте переменные окружения
echo $CELERY_BROKER_URL
```
**Задача не выполняется:**
```bash
# Проверьте FlareSolver
curl http://localhost:8191/v1
# Проверьте логи worker
tail -f dbapp/logs/celery_worker.log
```
**Прогресс не обновляется:**
- Откройте консоль браузера (F12)
- Проверьте Network tab на наличие ошибок
- Обновите страницу

View File

@@ -1,135 +0,0 @@
# Task 28 Completion Summary: Optimize SourceListView Queries
## ✅ Task Status: COMPLETED
## Objective
Optimize SQL queries in SourceListView to eliminate N+1 query problems and improve performance by using Django ORM optimization techniques.
## What Was Done
### 1. Added select_related() for ForeignKey/OneToOne Relationships
Enhanced the queryset to fetch related objects using SQL JOINs:
- `info` (ForeignKey to ObjectInfo)
- `created_by` and `created_by__user` (ForeignKey to CustomUser → User)
- `updated_by` and `updated_by__user` (ForeignKey to CustomUser → User)
### 2. Added prefetch_related() for Reverse ForeignKey and ManyToMany
Implemented comprehensive prefetching for all related collections:
- All `source_objitems` with nested relationships:
- `parameter_obj` and its related fields (satellite, polarization, modulation, standard)
- `geo_obj` and its mirrors (ManyToMany)
- `lyngsat_source` and its satellite
- `transponder`
- `created_by` and `updated_by` with their users
- All `marks` with their `created_by` relationships
### 3. Used annotate() for Efficient Counting
Implemented database-level counting using `Count()` aggregation:
- Counts `objitem_count` in the database using GROUP BY
- Supports filtered counting when filters are applied
- Eliminates need for Python-level counting loops
## Results
### Query Performance
- **Total queries**: 22 (constant)
- **Scaling**: Perfect - query count remains at 22 regardless of page size
- **Status**: ✅ EXCELLENT
### Test Results
| Page Size | Query Count | Variation |
|-----------|-------------|-----------|
| 10 items | 22 queries | 0 |
| 50 items | 22 queries | 0 |
| 100 items | 22 queries | 0 |
### Performance Improvement
- **Before**: ~100-1000+ queries (N+1 problem, scales with items)
- **After**: 22 queries (constant, no scaling)
- **Improvement**: 95-98% reduction in query count
## Requirements Compliance
**Requirement 8.1**: Minimize SQL queries to database
**Requirement 8.2**: Use select_related() for ForeignKey/OneToOne
**Requirement 8.3**: Use prefetch_related() for ManyToMany and reverse ForeignKey
**Requirement 8.4**: Use annotate() instead of multiple queries in loops
**Requirement 8.6**: Reduce query count by at least 50% (achieved 95-98%)
## Files Modified
### Production Code
- `dbapp/mainapp/views/source.py`: Updated SourceListView.get() method with optimized queryset
### Test Files Created
- `test_source_query_optimization.py`: Basic query count verification
- `test_source_query_detailed.py`: Detailed query analysis with SQL output
- `test_source_query_scale.py`: Scaling test across different page sizes
### Documentation
- `OPTIMIZATION_REPORT_SourceListView.md`: Comprehensive optimization report
- `TASK_28_COMPLETION_SUMMARY.md`: This summary document
## Verification
All optimizations have been verified through automated testing:
1. ✅ Query count is stable at 22 regardless of page size
2. ✅ No N+1 query problems detected
3. ✅ All relationships properly optimized with select_related/prefetch_related
4. ✅ Counting uses database-level aggregation
## Code Changes
The main optimization in `dbapp/mainapp/views/source.py`:
```python
sources = Source.objects.select_related(
'info',
'created_by',
'created_by__user',
'updated_by',
'updated_by__user',
).prefetch_related(
'source_objitems',
'source_objitems__parameter_obj',
'source_objitems__parameter_obj__id_satellite',
'source_objitems__parameter_obj__polarization',
'source_objitems__parameter_obj__modulation',
'source_objitems__parameter_obj__standard',
'source_objitems__geo_obj',
'source_objitems__geo_obj__mirrors',
'source_objitems__lyngsat_source',
'source_objitems__lyngsat_source__satellite',
'source_objitems__transponder',
'source_objitems__created_by',
'source_objitems__created_by__user',
'source_objitems__updated_by',
'source_objitems__updated_by__user',
'marks',
'marks__created_by',
'marks__created_by__user'
).annotate(
objitem_count=Count('source_objitems', filter=objitem_filter_q, distinct=True)
if has_objitem_filter
else Count('source_objitems')
)
```
## Next Steps
This optimization pattern should be applied to other list views:
- Task 29: ObjItemListView
- Task 30: TransponderListView
- Task 31: LyngsatListView
- Task 32: ObjectMarksListView
## Conclusion
Task 28 has been successfully completed with excellent results. The SourceListView now uses optimal Django ORM patterns to minimize database queries, resulting in a 95-98% reduction in query count and eliminating all N+1 query problems.
---
**Completed**: 2025-11-18
**Developer**: Kiro AI Assistant
**Status**: ✅ VERIFIED AND COMPLETE

View File

@@ -1,154 +0,0 @@
# Страница Кубсат
## Описание
Страница "Кубсат" предназначена для фильтрации источников сигнала (Source) по различным критериям и экспорта результатов в Excel.
## Доступ
Страница доступна по адресу: `/kubsat/`
Также добавлена в навигационное меню между "Наличие сигнала" и "3D карта".
## Функциональность
### Фильтры
#### Реализованные фильтры:
1. **Спутники** (мультивыбор) - фильтрация по спутникам из связанных ObjItem
2. **Полоса спутника** (выбор) - выбор диапазона частот
3. **Поляризация** (мультивыбор) - фильтрация по поляризации сигнала
4. **Центральная частота** (диапазон) - фильтрация по частоте от/до в МГц
5. **Полоса** (диапазон) - фильтрация по полосе частот от/до в МГц
6. **Модуляция** (мультивыбор) - фильтрация по типу модуляции
7. **Тип объекта** (мультивыбор) - фильтрация по типу объекта (ObjectInfo)
8. **Количество привязанных ObjItem** (radio button):
- Все
- 1
- 2 и более
#### Фиктивные фильтры (заглушки):
9. **Принадлежность объекта** (мультивыбор) - пока не реализовано
10. **Планы на** (radio да/нет) - фиктивный фильтр
11. **Успех 1** (radio да/нет) - фиктивный фильтр
12. **Успех 2** (radio да/нет) - фиктивный фильтр
13. **Диапазон дат** (от/до) - фиктивный фильтр
### Таблица результатов
После применения фильтров отображается таблица на всю ширину страницы с колонками:
- ID Source - идентификатор источника (объединяет несколько точек)
- Тип объекта - тип источника
- Кол-во точек - количество точек источника (автоматически пересчитывается при удалении)
- Имя точки - название точки (ObjItem)
- Спутник (имя и NORAD ID)
- Частота (МГц)
- Полоса (МГц)
- Поляризация
- Модуляция
- Координаты ГЛ - координаты из geo_obj точки
- Дата ГЛ - дата геолокации точки
- Действия (кнопки удаления)
**Важно**:
- Таблица показывает все точки (ObjItem) для каждого источника (Source)
- Точки одного источника группируются вместе
- Колонки ID Source, Тип объекта и Кол-во точек объединены для всех строк источника (rowspan)
- Количество точек автоматически пересчитывается при удалении строк
- Таблица имеет фиксированную высоту с прокруткой и sticky заголовок
### Двухэтапная фильтрация
Фильтрация происходит в два этапа:
**Этап 1: Фильтрация по дате ГЛ**
- Если задан диапазон дат (от/до), отображаются только те точки, у которых дата ГЛ попадает в этот диапазон
- Точки без даты ГЛ исключаются, если фильтр по дате задан
- Если фильтр не задан, показываются все точки
**Этап 2: Фильтрация по количеству точек**
- Применяется к уже отфильтрованным по дате точкам
- Варианты:
- "Все" - показываются источники с любым количеством точек
- "1" - только источники с ровно 1 точкой (после фильтрации по дате)
- "2 и более" - только источники с 2 и более точками (после фильтрации по дате)
**Важно**: Фильтр по количеству точек учитывает только те точки, которые прошли фильтрацию по дате!
### Управление данными в таблице
**Кнопки удаления:**
- **Кнопка с иконкой корзины** (для каждой точки) - удаляет конкретную точку (ObjItem) из таблицы
- **Кнопка с иконкой заполненной корзины** (для первой точки источника) - удаляет все точки источника (Source) из таблицы
**Важно**: Все удаления происходят только из таблицы, **БЕЗ удаления из базы данных**. Это позволяет пользователю исключить ненужные записи перед экспортом.
### Экспорт в Excel
Кнопка "Экспорт в Excel" создает файл со следующими колонками:
1. **Дата** - текущая дата (без времени)
2. **Широта, град** - рассчитывается как **инкрементальное среднее** из координат оставшихся в таблице точек
3. **Долгота, град** - рассчитывается как **инкрементальное среднее** из координат оставшихся в таблице точек
4. **Высота, м** - всегда 0
5. **Местоположение** - из geo_obj.location первого ObjItem
6. **ИСЗ** - имя спутника и NORAD ID в скобках
7. **Прямой канал, МГц** - частота + перенос из транспондера
8. **Обратный канал, МГц** - частота источника
9. **Перенос** - из объекта Transponder
10. **Получено координат, раз** - количество точек (ObjItem), оставшихся в таблице для данного источника
11. **Период получения координат** - диапазон дат ГЛ в формате "5.11.2025-15.11.2025" (от самой ранней до самой поздней даты среди точек источника). Если все точки имеют одну дату, показывается только одна дата.
12. **Зеркала** - все имена зеркал через перенос строки (из оставшихся точек)
13. **СКО, км** - не заполняется
14. **Примечание** - не заполняется
15. **Оператор** - имя текущего пользователя
**Важно**:
- Экспортируются только точки (ObjItem), оставшиеся в таблице после удалений
- Координаты рассчитываются по алгоритму инкрементального среднего из функции `calculate_mean_coords` (аналогично `fill_data_from_df`)
- Если пользователь удалил некоторые точки, координаты будут рассчитаны только по оставшимся
Файл сохраняется с именем `kubsat_YYYYMMDD_HHMMSS.xlsx`.
## Технические детали
### Файлы
- **Форма**: `dbapp/mainapp/forms.py` - класс `KubsatFilterForm`
- **Представления**: `dbapp/mainapp/views/kubsat.py` - классы `KubsatView` и `KubsatExportView`
- **Шаблон**: `dbapp/mainapp/templates/mainapp/kubsat.html`
- **URL**: `/kubsat/` и `/kubsat/export/`
### Зависимости
- openpyxl - для создания Excel файлов
- Django GIS - для работы с координатами
### Оптимизация запросов
Используется `select_related` и `prefetch_related` для минимизации количества запросов к базе данных:
```python
queryset = Source.objects.select_related('info').prefetch_related(
'source_objitems__parameter_obj__id_satellite',
'source_objitems__parameter_obj__polarization',
'source_objitems__parameter_obj__modulation',
'source_objitems__transponder__sat_id'
)
```
## Использование
1. Откройте страницу "Кубсат" из навигационного меню
2. Выберите нужные фильтры (спутники, поляризация, частота и т.д.)
3. Опционально укажите диапазон дат для фильтрации точек по дате ГЛ (Этап 1)
4. Опционально выберите количество точек (1 или 2+) - применяется к отфильтрованным по дате точкам (Этап 2)
5. Нажмите "Применить фильтры"
6. В таблице отобразятся точки (ObjItem) сгруппированные по источникам (Source)
7. При необходимости удалите отдельные точки или целые объекты кнопками в колонке "Действия"
8. Нажмите "Экспорт в Excel" для скачивания файла с оставшимися данными
9. Форма не сбрасывается после экспорта - можно продолжить работу
## Примечания
- Форма не сбрасывается после экспорта
- Удаление точек/объектов из таблицы не влияет на базу данных
- Экспортируются только оставшиеся в таблице точки
- Координаты в Excel рассчитываются как инкрементальное среднее из оставшихся точек
- Фильтр по дате скрывает неподходящие точки (не показывает их в таблице)
- Каждая строка таблицы = одна точка (ObjItem), строки группируются по источникам (Source)
- Количество точек в колонке "Кол-во точек" автоматически пересчитывается при удалении строк

View File

@@ -23,14 +23,12 @@ from .models import (
Polarization,
Modulation,
Standard,
SigmaParMark,
ObjectMark,
ObjectInfo,
ObjectOwnership,
SigmaParameter,
Parameter,
Satellite,
Mirror,
Geo,
ObjItem,
CustomUser,
@@ -359,14 +357,14 @@ class ObjectMarkAdmin(BaseAdmin):
autocomplete_fields = ("source",)
@admin.register(SigmaParMark)
class SigmaParMarkAdmin(BaseAdmin):
"""Админ-панель для модели SigmaParMark."""
# @admin.register(SigmaParMark)
# class SigmaParMarkAdmin(BaseAdmin):
# """Админ-панель для модели SigmaParMark."""
list_display = ("mark", "timestamp")
search_fields = ("mark",)
ordering = ("-timestamp",)
list_filter = (("timestamp", DateRangeQuickSelectListFilterBuilder()),)
# list_display = ("mark", "timestamp")
# search_fields = ("mark",)
# ordering = ("-timestamp",)
# list_filter = (("timestamp", DateRangeQuickSelectListFilterBuilder()),)
@admin.register(Polarization)
@@ -417,7 +415,6 @@ class ObjectOwnershipAdmin(BaseAdmin):
class SigmaParameterInline(admin.StackedInline):
model = SigmaParameter
extra = 0
autocomplete_fields = ["mark"]
readonly_fields = (
"datetime_begin",
"datetime_end",
@@ -561,7 +558,6 @@ class SigmaParameterAdmin(ImportExportActionModelAdmin, BaseAdmin):
"modulation__name",
"standard__name",
)
autocomplete_fields = ("mark",)
ordering = ("-frequency",)
def get_queryset(self, request):
@@ -589,13 +585,13 @@ class SatelliteAdmin(BaseAdmin):
readonly_fields = ("created_at", "created_by", "updated_at", "updated_by")
@admin.register(Mirror)
class MirrorAdmin(ImportExportActionModelAdmin, BaseAdmin):
"""Админ-панель для модели Mirror с поддержкой импорта/экспорта."""
# @admin.register(Mirror)
# class MirrorAdmin(ImportExportActionModelAdmin, BaseAdmin):
# """Админ-панель для модели Mirror с поддержкой импорта/экспорта."""
list_display = ("name",)
search_fields = ("name",)
ordering = ("name",)
# list_display = ("name",)
# search_fields = ("name",)
# ordering = ("name",)
@admin.register(Geo)

View File

@@ -172,63 +172,63 @@ class ObjectMark(models.Model):
# Для обратной совместимости с SigmaParameter
class SigmaParMark(models.Model):
"""
Модель отметки о наличии сигнала (для Sigma).
# class SigmaParMark(models.Model):
# """
# Модель отметки о наличии сигнала (для Sigma).
Используется для фиксации моментов времени когда сигнал был обнаружен или потерян.
"""
# Используется для фиксации моментов времени когда сигнал был обнаружен или потерян.
# """
# Основные поля
mark = models.BooleanField(
null=True,
blank=True,
verbose_name="Наличие сигнала",
help_text="True - сигнал обнаружен, False - сигнал отсутствует",
)
timestamp = models.DateTimeField(
null=True,
blank=True,
verbose_name="Время",
db_index=True,
help_text="Время фиксации отметки",
)
# # Основные поля
# mark = models.BooleanField(
# null=True,
# blank=True,
# verbose_name="Наличие сигнала",
# help_text="True - сигнал обнаружен, False - сигнал отсутствует",
# )
# timestamp = models.DateTimeField(
# null=True,
# blank=True,
# verbose_name="Время",
# db_index=True,
# help_text="Время фиксации отметки",
# )
def __str__(self):
if self.timestamp:
timestamp = self.timestamp.strftime("%d.%m.%Y %H:%M")
return f"+ {timestamp}" if self.mark else f"- {timestamp}"
return "Отметка без времени"
# def __str__(self):
# if self.timestamp:
# timestamp = self.timestamp.strftime("%d.%m.%Y %H:%M")
# return f"+ {timestamp}" if self.mark else f"- {timestamp}"
# return "Отметка без времени"
class Meta:
verbose_name = "Отметка сигнала"
verbose_name_plural = "Отметки сигналов"
ordering = ["-timestamp"]
# class Meta:
# verbose_name = "Отметка сигнала"
# verbose_name_plural = "Отметки сигналов"
# ordering = ["-timestamp"]
class Mirror(models.Model):
"""
Модель зеркала антенны.
# class Mirror(models.Model):
# """
# Модель зеркала антенны.
Представляет физическое зеркало антенны для приема спутникового сигнала.
"""
# Представляет физическое зеркало антенны для приема спутникового сигнала.
# """
# Основные поля
name = models.CharField(
max_length=30,
unique=True,
verbose_name="Имя зеркала",
db_index=True,
help_text="Уникальное название зеркала антенны",
)
# # Основные поля
# name = models.CharField(
# max_length=30,
# unique=True,
# verbose_name="Имя зеркала",
# db_index=True,
# help_text="Уникальное название зеркала антенны",
# )
def __str__(self):
return self.name
# def __str__(self):
# return self.name
class Meta:
verbose_name = "Зеркало"
verbose_name_plural = "Зеркала"
ordering = ["name"]
# class Meta:
# verbose_name = "Зеркало"
# verbose_name_plural = "Зеркала"
# ordering = ["name"]
class Polarization(models.Model):
@@ -1027,7 +1027,7 @@ class SigmaParameter(models.Model):
verbose_name="Время окончания измерения",
help_text="Дата и время окончания измерения",
)
mark = models.ManyToManyField(SigmaParMark, verbose_name="Отметка", blank=True)
# mark = models.ManyToManyField(SigmaParMark, verbose_name="Отметка", blank=True)
parameter = models.ForeignKey(
Parameter,
on_delete=models.SET_NULL,

View File

@@ -212,26 +212,26 @@
<div class="card">
<div class="card-body">
<h4>Частотный план</h4>
<p class="text-muted">Визуализация транспондеров спутника по частотам (Downlink). Используйте колесико мыши для масштабирования, наведите курсор на полосу для подробной информации.</p>
<p class="text-muted">Визуализация транспондеров спутника по частотам. <span style="color: #0d6efd;"></span> Downlink (синий), <span style="color: #fd7e14;"></span> Uplink (оранжевый). Используйте колесико мыши для масштабирования, наведите курсор на полосу для подробной информации.</p>
<div class="frequency-plan">
<div class="chart-controls">
<button type="button" class="btn btn-sm btn-outline-primary" id="resetZoom">
<i class="bi bi-arrow-clockwise"></i> Сбросить масштаб
</button>
<button type="button" class="btn btn-sm btn-outline-secondary" id="zoomIn">
<!-- <button type="button" class="btn btn-sm btn-outline-secondary" id="zoomIn">
<i class="bi bi-zoom-in"></i> Увеличить
</button>
<button type="button" class="btn btn-sm btn-outline-secondary" id="zoomOut">
<i class="bi bi-zoom-out"></i> Уменьшить
</button>
</button> -->
</div>
<div class="frequency-chart-container">
<canvas id="frequencyChart"></canvas>
</div>
<div class="legend">
<!-- <div class="legend">
<div class="legend-item">
<div class="legend-color" style="background-color: #0d6efd;"></div>
<span>H - Горизонтальная</span>
@@ -252,7 +252,7 @@
<div class="legend-color" style="background-color: #6c757d;"></div>
<span>Другая</span>
</div>
</div>
</div> -->
<div class="mt-3">
<p><strong>Всего транспондеров:</strong> {{ transponder_count }}</p>
@@ -310,19 +310,28 @@ function initializeFrequencyChart() {
container = canvas.parentElement;
ctx = canvas.getContext('2d');
// Calculate frequency range
// Calculate frequency range (including both downlink and uplink)
minFreq = Infinity;
maxFreq = -Infinity;
transpondersData.forEach(t => {
const startFreq = t.downlink - (t.frequency_range / 2);
const endFreq = t.downlink + (t.frequency_range / 2);
minFreq = Math.min(minFreq, startFreq);
maxFreq = Math.max(maxFreq, endFreq);
// Downlink
const dlStartFreq = t.downlink - (t.frequency_range / 2);
const dlEndFreq = t.downlink + (t.frequency_range / 2);
minFreq = Math.min(minFreq, dlStartFreq);
maxFreq = Math.max(maxFreq, dlEndFreq);
// Uplink (if exists)
if (t.uplink) {
const ulStartFreq = t.uplink - (t.frequency_range / 2);
const ulEndFreq = t.uplink + (t.frequency_range / 2);
minFreq = Math.min(minFreq, ulStartFreq);
maxFreq = Math.max(maxFreq, ulEndFreq);
}
});
// Add 2% padding
const padding = (maxFreq - minFreq) * 0.02;
const padding = (maxFreq - minFreq) * 0.04;
minFreq -= padding;
maxFreq += padding;
@@ -368,10 +377,12 @@ function renderChart() {
const chartWidth = width - leftMargin - rightMargin;
const chartHeight = height - topMargin - bottomMargin;
// Group transponders by polarization
// Group transponders by polarization (use first letter only)
const polarizationGroups = {};
transpondersData.forEach(t => {
const pol = t.polarization || 'Другая';
let pol = t.polarization || '-';
// Take only first letter for abbreviation
pol = pol.charAt(0).toUpperCase();
if (!polarizationGroups[pol]) {
polarizationGroups[pol] = [];
}
@@ -379,7 +390,8 @@ function renderChart() {
});
const polarizations = Object.keys(polarizationGroups);
const rowHeight = chartHeight / polarizations.length;
// Each polarization gets 2 rows (downlink + uplink)
const rowHeight = chartHeight / (polarizations.length * 2);
// Calculate visible frequency range with zoom and pan
const visibleFreqRange = freqRange / zoomLevel;
@@ -443,18 +455,41 @@ function renderChart() {
// Draw transponders
polarizations.forEach((pol, index) => {
const group = polarizationGroups[pol];
const color = getColor(pol);
const y = topMargin + index * rowHeight;
const barHeight = rowHeight * 0.7;
const barY = y + (rowHeight - barHeight) / 2;
const downlinkColor = '#0000ff'; //getColor(pol);
const uplinkColor = '#fd7e14';
// Draw polarization label
// Downlink row
const downlinkY = topMargin + (index * 2) * rowHeight;
const downlinkBarHeight = rowHeight * 0.8;
const downlinkBarY = downlinkY + (rowHeight - downlinkBarHeight) / 2;
// Uplink row
const uplinkY = topMargin + (index * 2 + 1) * rowHeight;
const uplinkBarHeight = rowHeight * 0.8;
const uplinkBarY = uplinkY + (rowHeight - uplinkBarHeight) / 2;
// Draw polarization label (centered between downlink and uplink)
ctx.fillStyle = '#000';
ctx.font = 'bold 12px sans-serif';
ctx.textAlign = 'right';
ctx.fillText(pol, leftMargin - 10, barY + barHeight / 2 + 4);
ctx.font = 'bold 14px sans-serif';
ctx.textAlign = 'center';
const labelY = downlinkY + rowHeight;
ctx.fillText(pol, leftMargin - 25, labelY);
// Draw transponders
// Draw "DL" and "UL" labels
ctx.font = '10px sans-serif';
ctx.fillStyle = '#666';
ctx.fillText('DL', leftMargin - 5, downlinkBarY + downlinkBarHeight / 2 + 3);
ctx.fillText('UL', leftMargin - 5, uplinkBarY + uplinkBarHeight / 2 + 3);
// Draw separator line between DL and UL
ctx.strokeStyle = '#dee2e6';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(leftMargin, uplinkY);
ctx.lineTo(width - rightMargin, uplinkY);
ctx.stroke();
// Draw downlink transponders
group.forEach(t => {
const startFreq = t.downlink - (t.frequency_range / 2);
const endFreq = t.downlink + (t.frequency_range / 2);
@@ -464,6 +499,52 @@ function renderChart() {
return;
}
const x1 = leftMargin + ((startFreq - visibleMinFreq) / (visibleMaxFreq - visibleMinFreq)) * chartWidth;
const x2 = leftMargin + ((endFreq - visibleMinFreq) / (visibleMaxFreq - visibleMinFreq)) * chartWidth;
const barWidth = x2 - x1;
if (barWidth < 1) return;
// Draw downlink bar
ctx.fillStyle = downlinkColor;
ctx.fillRect(x1, downlinkBarY, barWidth, downlinkBarHeight);
// Draw border
ctx.strokeStyle = '#fff';
ctx.lineWidth = 1;
ctx.strokeRect(x1, downlinkBarY, barWidth, downlinkBarHeight);
// Draw name if there's space
if (barWidth > 40) {
ctx.fillStyle = (pol === 'R') ? '#000' : '#fff';
ctx.font = '9px sans-serif';
ctx.textAlign = 'center';
ctx.fillText(t.name, x1 + barWidth / 2, downlinkBarY + downlinkBarHeight / 2 + 3);
}
// Store for hover detection
transponderRects.push({
x: x1,
y: downlinkBarY,
width: barWidth,
height: downlinkBarHeight,
transponder: t,
type: 'downlink'
});
});
// Draw uplink transponders
group.forEach(t => {
if (!t.uplink) return; // Skip if no uplink data
const startFreq = t.uplink - (t.frequency_range / 2);
const endFreq = t.uplink + (t.frequency_range / 2);
// Check if transponder is visible
if (endFreq < visibleMinFreq || startFreq > visibleMaxFreq) {
return;
}
// Calculate position
const x1 = leftMargin + ((startFreq - visibleMinFreq) / (visibleMaxFreq - visibleMinFreq)) * chartWidth;
const x2 = leftMargin + ((endFreq - visibleMinFreq) / (visibleMaxFreq - visibleMinFreq)) * chartWidth;
@@ -472,32 +553,44 @@ function renderChart() {
// Skip if too small
if (barWidth < 1) return;
// Draw bar
ctx.fillStyle = color;
ctx.fillRect(x1, barY, barWidth, barHeight);
// Draw uplink bar
ctx.fillStyle = uplinkColor;
ctx.fillRect(x1, uplinkBarY, barWidth, uplinkBarHeight);
// Draw border
ctx.strokeStyle = '#fff';
ctx.lineWidth = 2;
ctx.strokeRect(x1, barY, barWidth, barHeight);
ctx.lineWidth = 1;
ctx.strokeRect(x1, uplinkBarY, barWidth, uplinkBarHeight);
// Draw name if there's space
if (barWidth > 40) {
ctx.fillStyle = (pol === 'R') ? '#000' : '#fff';
ctx.font = '10px sans-serif';
ctx.fillStyle = '#fff';
ctx.font = '9px sans-serif';
ctx.textAlign = 'center';
ctx.fillText(t.name, x1 + barWidth / 2, barY + barHeight / 2 + 3);
ctx.fillText(t.name, x1 + barWidth / 2, uplinkBarY + uplinkBarHeight / 2 + 3);
}
// Store for hover detection
transponderRects.push({
x: x1,
y: barY,
y: uplinkBarY,
width: barWidth,
height: barHeight,
transponder: t
height: uplinkBarHeight,
transponder: t,
type: 'uplink'
});
});
// Draw separator line after each polarization group (except last)
if (index < polarizations.length - 1) {
const separatorY = topMargin + (index * 2 + 2) * rowHeight;
ctx.strokeStyle = '#adb5bd';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(leftMargin, separatorY);
ctx.lineTo(width - rightMargin, separatorY);
ctx.stroke();
}
});
// Draw hover tooltip
@@ -506,19 +599,29 @@ function renderChart() {
}
}
function drawTooltip(t) {
const startFreq = t.downlink - (t.frequency_range / 2);
const endFreq = t.downlink + (t.frequency_range / 2);
function drawTooltip(rectInfo) {
const t = rectInfo.transponder;
const isUplink = rectInfo.type === 'uplink';
const freq = isUplink ? t.uplink : t.downlink;
const startFreq = freq - (t.frequency_range / 2);
const endFreq = freq + (t.frequency_range / 2);
const lines = [
t.name,
'Тип: ' + (isUplink ? 'Uplink' : 'Downlink'),
'Диапазон: ' + startFreq.toFixed(3) + ' - ' + endFreq.toFixed(3) + ' МГц',
'Downlink: ' + t.downlink.toFixed(3) + ' МГц',
'Центр: ' + freq.toFixed(3) + ' МГц',
'Полоса: ' + t.frequency_range.toFixed(3) + ' МГц',
'Поляризация: ' + t.polarization,
'Зона: ' + t.zone_name
];
// Add frequency conversion info for uplink
if (isUplink && t.downlink && t.uplink) {
const conversion = t.downlink - t.uplink;
lines.push('Перенос: ' + conversion.toFixed(3) + ' МГц');
}
// Calculate tooltip size
ctx.font = '12px sans-serif';
const padding = 10;
@@ -533,8 +636,8 @@ function drawTooltip(t) {
const tooltipHeight = lines.length * lineHeight + padding * 2;
// Position tooltip
const mouseX = hoveredTransponder._mouseX || canvas.width / 2;
const mouseY = hoveredTransponder._mouseY || canvas.height / 2;
const mouseX = rectInfo._mouseX || canvas.width / 2;
const mouseY = rectInfo._mouseY || canvas.height / 2;
let tooltipX = mouseX + 15;
let tooltipY = mouseY + 15;
@@ -607,7 +710,7 @@ function handleMouseMove(e) {
for (const tr of transponderRects) {
if (mouseX >= tr.x && mouseX <= tr.x + tr.width &&
mouseY >= tr.y && mouseY <= tr.y + tr.height) {
found = tr.transponder;
found = tr;
found._mouseX = mouseX;
found._mouseY = mouseY;
break;
@@ -662,8 +765,8 @@ document.addEventListener('DOMContentLoaded', function() {
// Control buttons
document.getElementById('resetZoom').addEventListener('click', resetZoom);
document.getElementById('zoomIn').addEventListener('click', zoomIn);
document.getElementById('zoomOut').addEventListener('click', zoomOut);
// document.getElementById('zoomIn').addEventListener('click', zoomIn);
// document.getElementById('zoomOut').addEventListener('click', zoomOut);
});
// Re-render on window resize

View File

@@ -17,7 +17,6 @@ from mapsapp.models import Transponders
from .models import (
CustomUser,
Geo,
Mirror,
Modulation,
ObjItem,
Parameter,
@@ -126,9 +125,9 @@ def get_all_constants():
sats = [sat.name for sat in Satellite.objects.all()]
standards = [sat.name for sat in Standard.objects.all()]
pols = [sat.name for sat in Polarization.objects.all()]
mirrors = [sat.name for sat in Mirror.objects.all()]
# mirrors = [sat.name for sat in Mirror.objects.all()]
modulations = [sat.name for sat in Modulation.objects.all()]
return sats, standards, pols, mirrors, modulations
return sats, standards, pols, modulations
def find_mirror_satellites(mirror_names: list) -> list:

View File

@@ -259,6 +259,7 @@ class SatelliteUpdateView(RoleRequiredMixin, FormMessageMixin, UpdateView):
'id': t.id,
'name': t.name or f"TP-{t.id}",
'downlink': float(t.downlink),
'uplink': float(t.uplink) if t.uplink else None,
'frequency_range': float(t.frequency_range),
'polarization': t.polarization.name if t.polarization else '-',
'zone_name': t.zone_name or '-',