diff --git a/docs/docs/documentation/getting-started/installation/backend-config.md b/docs/docs/documentation/getting-started/installation/backend-config.md index 15e31e43649e..11490e62d94c 100644 --- a/docs/docs/documentation/getting-started/installation/backend-config.md +++ b/docs/docs/documentation/getting-started/installation/backend-config.md @@ -4,20 +4,20 @@ ### General -| Variables | Default | Description | -| ----------------------------- | :-------------------: | ----------------------------------------------------------------------------------- | -| PUID | 911 | UserID permissions between host OS and container | -| PGID | 911 | GroupID permissions between host OS and container | -| DEFAULT_GROUP | Home | The default group for users | -| BASE_URL | http://localhost:8080 | Used for Notifications | -| TOKEN_TIME | 48 | The time in hours that a login/auth token is valid | -| API_PORT | 9000 | The port exposed by backend API. **Do not change this if you're running in Docker** | -| API_DOCS | True | Turns on/off access to the API documentation locally. | -| TZ | UTC | Must be set to get correct date/time on the server | -| ALLOW_SIGNUP\* | false | Allow user sign-up without token | -| LOG_CONFIG_OVERRIDE | | Override the config for logging with a custom path | -| LOG_LEVEL | info | Logging level (e.g. critical, error, warning, info, debug, trace) | -| DAILY_SCHEDULE_TIME | 23:45 | The time of day to run the daily tasks. | +| Variables | Default | Description | +| ----------------------------- | :-------------------: | --------------------------------------------------------------------------------------------------------- | +| PUID | 911 | UserID permissions between host OS and container | +| PGID | 911 | GroupID permissions between host OS and container | +| DEFAULT_GROUP | Home | The default group for users | +| BASE_URL | http://localhost:8080 | Used for Notifications | +| TOKEN_TIME | 48 | The time in hours that a login/auth token is valid | +| API_PORT | 9000 | The port exposed by backend API. **Do not change this if you're running in Docker** | +| API_DOCS | True | Turns on/off access to the API documentation locally | +| TZ | UTC | Must be set to get correct date/time on the server | +| ALLOW_SIGNUP\* | false | Allow user sign-up without token | +| LOG_CONFIG_OVERRIDE | | Override the config for logging with a custom path | +| LOG_LEVEL | info | Logging level (e.g. critical, error, warning, info, debug, trace) | +| DAILY_SCHEDULE_TIME | 23:45 | The time of day to run daily server tasks, in HH:MM format. Use the server's local time, *not* UTC | \* Starting in v1.4.0 this was changed to default to `false` as part of a security review of the application. diff --git a/mealie/core/settings/settings.py b/mealie/core/settings/settings.py index eadd0c4014d7..1eae6a3a19c1 100644 --- a/mealie/core/settings/settings.py +++ b/mealie/core/settings/settings.py @@ -1,6 +1,10 @@ +import logging import secrets +from datetime import datetime, timezone from pathlib import Path +from typing import NamedTuple +from dateutil.tz import tzlocal from pydantic import field_validator from pydantic_settings import BaseSettings, SettingsConfigDict @@ -9,6 +13,11 @@ from mealie.core.settings.themes import Theme from .db_providers import AbstractDBProvider, db_provider_factory +class ScheduleTime(NamedTuple): + hour: int + minute: int + + def determine_secrets(data_dir: Path, production: bool) -> str: if not production: return "shh-secret-test-key" @@ -58,6 +67,44 @@ class AppSettings(BaseSettings): ALLOW_SIGNUP: bool = False DAILY_SCHEDULE_TIME: str = "23:45" + """Local server time, in HH:MM format. See `DAILY_SCHEDULE_TIME_UTC` for the parsed UTC equivalent""" + + _logger: logging.Logger | None = None + + @property + def logger(self) -> logging.Logger: + if self._logger is None: + # lazy load the logger, since get_logger depends on the settings being loaded + from mealie.core.root_logger import get_logger + + self._logger = get_logger() + + return self._logger + + @property + def DAILY_SCHEDULE_TIME_UTC(self) -> ScheduleTime: + """The DAILY_SCHEDULE_TIME in UTC, parsed into hours and minutes""" + + # parse DAILY_SCHEDULE_TIME into hours and minutes + try: + hour_str, minute_str = self.DAILY_SCHEDULE_TIME.split(":") + local_hour = int(hour_str) + local_minute = int(minute_str) + except ValueError: + local_hour = 23 + local_minute = 45 + self.logger.exception( + f"Unable to parse {self.DAILY_SCHEDULE_TIME=} as HH:MM; defaulting to {local_hour}:{local_minute}" + ) + + # DAILY_SCHEDULE_TIME is in local time, so we convert it to UTC + local_tz = tzlocal() + now = datetime.now(local_tz) + local_time = now.replace(hour=local_hour, minute=local_minute) + utc_time = local_time.astimezone(timezone.utc) + + self.logger.debug(f"Local time: {local_hour}:{local_minute} | UTC time: {utc_time.hour}:{utc_time.minute}") + return ScheduleTime(utc_time.hour, utc_time.minute) # =============================================== # Security Configuration diff --git a/mealie/services/scheduler/scheduler_service.py b/mealie/services/scheduler/scheduler_service.py index ce4fc9510ac9..893912205b7f 100644 --- a/mealie/services/scheduler/scheduler_service.py +++ b/mealie/services/scheduler/scheduler_service.py @@ -29,20 +29,12 @@ class SchedulerService: async def schedule_daily(): now = datetime.now(timezone.utc) - daily_schedule_time = get_app_settings().DAILY_SCHEDULE_TIME - logger.debug( - "Current time is %s and DAILY_SCHEDULE_TIME is %s", - str(now), - daily_schedule_time, - ) - try: - hour_target, minute_target = _parse_daily_schedule_time(daily_schedule_time) - except Exception: - logger.exception(f"Unable to parse {daily_schedule_time=}") - hour_target = 23 - minute_target = 45 + daily_schedule_time = get_app_settings().DAILY_SCHEDULE_TIME_UTC + logger.debug(f"Current time is {now} and DAILY_SCHEDULE_TIME (in UTC) is {daily_schedule_time}") - next_schedule = now.replace(hour=hour_target, minute=minute_target, second=0, microsecond=0) + next_schedule = now.replace( + hour=daily_schedule_time.hour, minute=daily_schedule_time.minute, second=0, microsecond=0 + ) delta = next_schedule - now if delta < timedelta(0): next_schedule = next_schedule + timedelta(days=1) @@ -61,12 +53,6 @@ async def schedule_daily(): await run_daily() -def _parse_daily_schedule_time(time): - hour_target = int(time.split(":")[0]) - minute_target = int(time.split(":")[1]) - return hour_target, minute_target - - def _scheduled_task_wrapper(callable): try: callable()