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/<id> (last_seen, created_at)
- Applied localization to GET /api/endpoints/<id>/scriptruns (execution times)
- Applied localization to GET /api/endpoints/<id>/logcollections (collection times)

This aligns endpoints plugin with notifications and backups plugins which
already use site_timezone for their time-sensitive operations.
This commit is contained in:
Proudlock
2026-09-11 12:22:31 -04:00
parent fb34b24d1c
commit c5b60024ed

View File

@@ -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/<int:collection_id>', methods=['GET'])