From c5b60024ed6e77bd7eaa7adf81f0636cee8161a4 Mon Sep 17 00:00:00 2001 From: Proudlock <270015376@geaerospace.com> Date: Fri, 11 Sep 2026 12:22:31 -0400 Subject: [PATCH] Add timezone localization support to endpoints plugin Endpoints plugin now respects the site_timezone setting from /settings. All timestamps in endpoint details, script runs, and log collections are now converted from UTC to the configured site timezone for display. Changes: - Added _get_site_timezone() helper to read site_timezone setting - Added _localize_timestamp() to convert UTC datetimes to site timezone - Applied localization to GET /api/endpoints/ (last_seen, created_at) - Applied localization to GET /api/endpoints//scriptruns (execution times) - Applied localization to GET /api/endpoints//logcollections (collection times) This aligns endpoints plugin with notifications and backups plugins which already use site_timezone for their time-sensitive operations. --- plugins/endpoints/api/routes.py | 51 ++++++++++++++++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/plugins/endpoints/api/routes.py b/plugins/endpoints/api/routes.py index 1ff529f6..ce16c266 100644 --- a/plugins/endpoints/api/routes.py +++ b/plugins/endpoints/api/routes.py @@ -43,6 +43,34 @@ def _naive_utcnow(): return datetime.now(timezone.utc).replace(tzinfo=None) +def _get_site_timezone(): + """Get configured site timezone from settings, fallback to UTC.""" + try: + import pytz + setting = Setting.query.filter_by(key='site_timezone').first() + if setting and setting.value: + return pytz.timezone(setting.value) + except Exception: + pass + return timezone.utc + + +def _localize_timestamp(utc_datetime): + """Convert naive UTC datetime to site timezone ISO string for display.""" + if not utc_datetime: + return None + try: + import pytz + site_tz = _get_site_timezone() + # Assume stored datetime is UTC (naive) + utc_aware = pytz.UTC.localize(utc_datetime) + localized = utc_aware.astimezone(site_tz) + return localized.isoformat() + except Exception: + # Fallback: return as UTC if anything fails + return utc_datetime.isoformat() + 'Z' if utc_datetime else None + + # What counts as RUNNING is defined once, by the reconciler, and imported here. # The panel and the reconciler read the same agent snapshot, so a disagreement # would paint a service green and flag it as drift on the same page. Importing @@ -440,6 +468,12 @@ def get_endpoint(asset_id: int): # the enqueue guard refuses against these same numbers, so a copy in the # frontend would silently disagree the moment a floor was raised here. data['actionminimums'] = action_minimum_versions() + + # Localize timestamps to site timezone + for ts_field in ['lastreportedat', 'lasteventat', 'created_at', 'updated_at']: + if ts_field in data and data[ts_field]: + data[ts_field] = _localize_timestamp(state.__dict__.get(ts_field)) + return success_response(data) @@ -2380,6 +2414,12 @@ def list_endpoint_script_runs(asset_id: int): for row in rows: data = row.to_dict(includedetail=True) data['scriptname'] = scriptnames.get(row.endpointcommandid) + + # Localize timestamps to site timezone + for ts_field in ['createdat', 'claimedat', 'completedat']: + if ts_field in data and data[ts_field]: + data[ts_field] = _localize_timestamp(row.__dict__.get(ts_field)) + payload.append(data) return success_response(payload) @@ -2404,7 +2444,16 @@ def list_endpoint_log_collections(asset_id: int): EndpointLogCollection.endpointlogcollectionid.desc()) .limit(EndpointLogCollection.RETENTION_PER_ASSET) .all()) - return success_response([row.to_dict() for row in rows]) + + # Localize timestamps to site timezone + payload = [] + for row in rows: + data = row.to_dict() + if 'collectedat' in data and data['collectedat']: + data['collectedat'] = _localize_timestamp(row.collectedat) + payload.append(data) + + return success_response(payload) @endpoints_bp.route('/logcollections/', methods=['GET'])