diff --git a/.trae/rules/main.md b/.trae/rules/main.md index 6937318..3d5b79e 100644 --- a/.trae/rules/main.md +++ b/.trae/rules/main.md @@ -14,7 +14,7 @@ - Для правок существующих файлов: всегда показывать diff‑превью и ждать принятия. ### Новые файлы -- Для новых файлов всегда давать: полное имя + абсолютный путь + полный контент одним блоком. +- Новые- файлы — строго один файл = один code block с абсолютным путём в заголовке; пояснения после блоков ### Безопасность - Никогда не логировать/не печатать: SECRET_KEY, пароли БД, токены. diff --git a/camlaps/camlaps/services/_ init _.py b/camlaps/camlaps/services/_ init _.py new file mode 100644 index 0000000..e69de29 diff --git a/camlaps/forms.py b/camlaps/forms.py new file mode 100644 index 0000000..7f0e1b4 --- /dev/null +++ b/camlaps/forms.py @@ -0,0 +1,34 @@ +from django import forms + +from .models import TimelapseJob + + +class TimelapseJobCreateForm(forms.ModelForm): + class Meta: + model = TimelapseJob + fields = ( + 'date_from', + 'date_to', + 'sampling_preset', + 'fps', + 'include_night', + 'day_start_time', + 'day_end_time', + ) + widgets = { + 'date_from': forms.DateInput(attrs={'type': 'date', 'class': 'form-control'}), + 'date_to': forms.DateInput(attrs={'type': 'date', 'class': 'form-control'}), + 'sampling_preset': forms.NumberInput( + attrs={ + 'type': 'range', + 'min': 0, + 'max': len(TimelapseJob.SAMPLING_PRESET_MINUTES) - 1, + 'step': 1, + 'class': 'form-range', + } + ), + 'fps': forms.NumberInput(attrs={'class': 'form-control', 'min': 1, 'max': 120}), + 'include_night': forms.CheckboxInput(attrs={'class': 'form-check-input'}), + 'day_start_time': forms.TimeInput(attrs={'type': 'time', 'class': 'form-control'}), + 'day_end_time': forms.TimeInput(attrs={'type': 'time', 'class': 'form-control'}), + } \ No newline at end of file diff --git a/camlaps/services/cameras.py b/camlaps/services/cameras.py new file mode 100644 index 0000000..0364cf1 --- /dev/null +++ b/camlaps/services/cameras.py @@ -0,0 +1,60 @@ +import logging +from pathlib import Path + +from django.conf import settings + +from ..models import Camera + + +logger = logging.getLogger('camlaps') + + +def _storage_root() -> Path: + storage_path = getattr(settings, 'STORAGE_PATH', None) + if storage_path is None: + return Path('/app/storage') + return Path(storage_path) + + +def is_storage_available() -> bool: + logger.info('cameras:storage_available:start') + try: + root = _storage_root() + ok = root.exists() and root.is_dir() + logger.info('cameras:storage_available:done ok=%s', ok) + return ok + except Exception: + logger.exception('cameras:storage_available:error') + raise + + +def list_active_cameras() -> list[Camera]: + logger.info('cameras:list_active:start') + try: + cameras = list(Camera.objects.filter(is_active=True).order_by('name')) + logger.info('cameras:list_active:done count=%s', len(cameras)) + return cameras + except Exception: + logger.exception('cameras:list_active:error') + raise + + +def get_camera_lastsnap_path(camera: Camera) -> Path | None: + logger.info('cameras:lastsnap_path:start camera_id=%s', camera.id) + try: + root = _storage_root().resolve() + candidate = (root / camera.storage_path / 'lastsnap.jpg').resolve() + + if candidate != root and root not in candidate.parents: + logger.info('cameras:lastsnap_path:done camera_id=%s found=false', camera.id) + return None + + if not candidate.exists() or not candidate.is_file(): + logger.info('cameras:lastsnap_path:done camera_id=%s found=false', camera.id) + return None + + logger.info('cameras:lastsnap_path:done camera_id=%s found=true', camera.id) + return candidate + except Exception: + logger.exception('cameras:lastsnap_path:error camera_id=%s', camera.id) + raise \ No newline at end of file diff --git a/camlaps/templates/camlaps/job_create.html b/camlaps/templates/camlaps/job_create.html new file mode 100644 index 0000000..8f17d33 --- /dev/null +++ b/camlaps/templates/camlaps/job_create.html @@ -0,0 +1,79 @@ +{% extends 'base.html' %} + +{% block content %} +
| ID | +Камера | +Период | +Интервал | +FPS | +Статус | +Прогресс | ++ |
|---|---|---|---|---|---|---|---|
| {{ job.id }} | +{{ job.camera.name }} | +{{ job.date_from }} → {{ job.date_to }} | +{{ job.get_sampling_preset_display }} | +{{ job.fps }} | +{{ job.get_status_display }} | ++ {% if job.status == 'success' %} + 100% + {% else %} + {{ job.progress_percent }}% + {% endif %} + | ++ Открыть + | +
| Пока нет задач. | |||||||