diff --git a/changedetectionio/blueprint/settings/__init__.py b/changedetectionio/blueprint/settings/__init__.py index a68d0646..03eb9f1f 100644 --- a/changedetectionio/blueprint/settings/__init__.py +++ b/changedetectionio/blueprint/settings/__init__.py @@ -1,8 +1,9 @@ import os from copy import deepcopy -from datetime import datetime +from datetime import datetime, timedelta from zoneinfo import ZoneInfo, available_timezones import secrets +import time import flask_login from flask import Blueprint, render_template, request, redirect, url_for, flash from flask_babel import gettext @@ -142,6 +143,9 @@ def construct_blueprint(datastore: ChangeDetectionStore): active_plugins = get_active_plugins() python_version = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}" + # Calculate uptime in seconds + uptime_seconds = time.time() - datastore.start_time + # Get plugin settings tabs and instantiate forms plugin_tabs = get_plugin_settings_tabs() plugin_forms = {} @@ -160,6 +164,7 @@ def construct_blueprint(datastore: ChangeDetectionStore): active_plugins=active_plugins, api_key=datastore.data['settings']['application'].get('api_access_token'), python_version=python_version, + uptime_seconds=uptime_seconds, available_timezones=sorted(available_timezones()), emailprefix=os.getenv('NOTIFICATION_MAIL_BUTTON_PREFIX', False), extra_notification_token_placeholder_info=datastore.get_unique_notification_token_placeholders_available(), diff --git a/changedetectionio/blueprint/settings/templates/settings.html b/changedetectionio/blueprint/settings/templates/settings.html index b6bf4b40..eec4293e 100644 --- a/changedetectionio/blueprint/settings/templates/settings.html +++ b/changedetectionio/blueprint/settings/templates/settings.html @@ -394,6 +394,7 @@ nav {% endfor %} {% endif %}
{{ _('Uptime:') }} {{ uptime_seconds|format_duration }}
{{ _('Python version:') }} {{ python_version }}
{{ _('Plugins active:') }}
{% if active_plugins %} diff --git a/changedetectionio/flask_app.py b/changedetectionio/flask_app.py index 8c25133d..6474468a 100644 --- a/changedetectionio/flask_app.py +++ b/changedetectionio/flask_app.py @@ -266,6 +266,47 @@ def _jinja2_filter_seconds_precise(timestamp): return format(int(time.time()-timestamp), ',d') +@app.template_filter('format_duration') +def _jinja2_filter_format_duration(seconds): + """Format a duration in seconds into human readable string like '5 days, 3 hours, 30 minutes'""" + from datetime import timedelta + + if not seconds or seconds < 0: + return gettext('0 seconds') + + td = timedelta(seconds=int(seconds)) + + # Calculate components + years = td.days // 365 + remaining_days = td.days % 365 + months = remaining_days // 30 + remaining_days = remaining_days % 30 + weeks = remaining_days // 7 + days = remaining_days % 7 + + hours = td.seconds // 3600 + minutes = (td.seconds % 3600) // 60 + secs = td.seconds % 60 + + # Build parts list + parts = [] + if years > 0: + parts.append(f"{years} {gettext('year') if years == 1 else gettext('years')}") + if months > 0: + parts.append(f"{months} {gettext('month') if months == 1 else gettext('months')}") + if weeks > 0: + parts.append(f"{weeks} {gettext('week') if weeks == 1 else gettext('weeks')}") + if days > 0: + parts.append(f"{days} {gettext('day') if days == 1 else gettext('days')}") + if hours > 0: + parts.append(f"{hours} {gettext('hour') if hours == 1 else gettext('hours')}") + if minutes > 0: + parts.append(f"{minutes} {gettext('minute') if minutes == 1 else gettext('minutes')}") + if secs > 0 or not parts: + parts.append(f"{secs} {gettext('second') if secs == 1 else gettext('seconds')}") + + return ", ".join(parts) + @app.template_filter('fetcher_status_icons') def _jinja2_filter_fetcher_status_icons(fetcher_name): """Get status icon HTML for a given fetcher.