mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-21 01:57:41 +02:00
Fix timestamps off by timezone offset in non-UTC containers (#504)
All backend timestamps used datetime.now() (server local time) or the deprecated datetime.utcnow(). The frontend's parseUTCDate() assumes timestamps without timezone indicators are UTC and appends 'Z', so stored timestamps were off by the timezone offset when the container's timezone wasn't UTC. Backend: replaced datetime.now() and datetime.utcnow() with datetime.now(timezone.utc) across 16 files (~80 call sites) for all database fields and DB comparisons. Cosmetic timestamps (filenames, user-facing local time formatting) intentionally left as local time. Frontend: replaced 13 new Date(backendTimestamp) calls with parseUTCDate() across 8 files to correctly interpret UTC timestamps.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
"""API routes for AMS sensor history."""
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from pydantic import BaseModel
|
||||
@@ -44,7 +44,7 @@ async def get_ams_history(
|
||||
_: User | None = RequirePermissionIfAuthEnabled(Permission.AMS_HISTORY_READ),
|
||||
):
|
||||
"""Get AMS sensor history for a specific printer and AMS unit."""
|
||||
since = datetime.now() - timedelta(hours=hours)
|
||||
since = datetime.now(timezone.utc) - timedelta(hours=hours)
|
||||
|
||||
# Get data points
|
||||
result = await db.execute(
|
||||
@@ -108,7 +108,7 @@ async def delete_old_history(
|
||||
_: User | None = RequirePermissionIfAuthEnabled(Permission.AMS_HISTORY_READ),
|
||||
):
|
||||
"""Delete old AMS history data for a printer."""
|
||||
cutoff = datetime.now() - timedelta(days=days)
|
||||
cutoff = datetime.now(timezone.utc) - timedelta(days=days)
|
||||
|
||||
result = await db.execute(
|
||||
select(func.count(AMSSensorHistory.id)).where(
|
||||
|
||||
@@ -1438,11 +1438,11 @@ async def scan_timelapse(
|
||||
# Strategy 4: If only one timelapse exists and archive was recently completed, use it
|
||||
# This handles cases where printer clock is wrong or timezone issues exist
|
||||
if not matching_file and len(video_files) == 1:
|
||||
from datetime import datetime, timedelta
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
archive_completed = archive.completed_at or archive.created_at
|
||||
if archive_completed:
|
||||
time_since_completion = datetime.now() - archive_completed
|
||||
time_since_completion = datetime.now(timezone.utc) - archive_completed
|
||||
# If archive was completed within the last hour, assume the single timelapse is for it
|
||||
if time_since_completion < timedelta(hours=1):
|
||||
matching_file = video_files[0]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Maintenance tracking API routes."""
|
||||
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy import select
|
||||
@@ -303,7 +303,7 @@ async def _get_printer_maintenance_internal(
|
||||
due_count = 0
|
||||
warning_count = 0
|
||||
|
||||
now = datetime.utcnow()
|
||||
now = datetime.now(timezone.utc)
|
||||
|
||||
for maint_type in all_types:
|
||||
# Skip system types that don't apply to this printer model
|
||||
@@ -591,7 +591,7 @@ async def perform_maintenance(
|
||||
db.add(history)
|
||||
|
||||
# Update item
|
||||
item.last_performed_at = datetime.utcnow()
|
||||
item.last_performed_at = datetime.now(timezone.utc)
|
||||
item.last_performed_hours = current_hours
|
||||
|
||||
await db.commit()
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import json
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy import delete, desc, func, select
|
||||
@@ -194,11 +194,11 @@ async def test_all_notification_providers(
|
||||
|
||||
# Update provider status
|
||||
if success:
|
||||
provider.last_success = datetime.utcnow()
|
||||
provider.last_success = datetime.now(timezone.utc)
|
||||
success_count += 1
|
||||
else:
|
||||
provider.last_error = message
|
||||
provider.last_error_at = datetime.utcnow()
|
||||
provider.last_error_at = datetime.now(timezone.utc)
|
||||
failed_count += 1
|
||||
|
||||
results.append(
|
||||
@@ -248,7 +248,7 @@ async def get_notification_logs(
|
||||
if success is not None:
|
||||
query = query.where(NotificationLog.success == success)
|
||||
if days is not None:
|
||||
cutoff = datetime.utcnow() - timedelta(days=days)
|
||||
cutoff = datetime.now(timezone.utc) - timedelta(days=days)
|
||||
query = query.where(NotificationLog.created_at >= cutoff)
|
||||
|
||||
query = query.offset(offset).limit(limit)
|
||||
@@ -295,7 +295,7 @@ async def get_notification_log_stats(
|
||||
_: User | None = RequirePermissionIfAuthEnabled(Permission.NOTIFICATIONS_READ),
|
||||
):
|
||||
"""Get notification log statistics."""
|
||||
cutoff = datetime.utcnow() - timedelta(days=days)
|
||||
cutoff = datetime.now(timezone.utc) - timedelta(days=days)
|
||||
|
||||
# Total counts
|
||||
total_result = await db.execute(select(func.count(NotificationLog.id)).where(NotificationLog.created_at >= cutoff))
|
||||
@@ -341,7 +341,7 @@ async def clear_notification_logs(
|
||||
_: User | None = RequirePermissionIfAuthEnabled(Permission.NOTIFICATIONS_DELETE),
|
||||
):
|
||||
"""Clear old notification logs."""
|
||||
cutoff = datetime.utcnow() - timedelta(days=older_than_days)
|
||||
cutoff = datetime.now(timezone.utc) - timedelta(days=older_than_days)
|
||||
|
||||
result = await db.execute(delete(NotificationLog).where(NotificationLog.created_at < cutoff))
|
||||
await db.commit()
|
||||
@@ -446,10 +446,10 @@ async def test_notification_provider(
|
||||
|
||||
# Update provider status
|
||||
if success:
|
||||
provider.last_success = datetime.utcnow()
|
||||
provider.last_success = datetime.now(timezone.utc)
|
||||
else:
|
||||
provider.last_error = message
|
||||
provider.last_error_at = datetime.utcnow()
|
||||
provider.last_error_at = datetime.now(timezone.utc)
|
||||
|
||||
await db.commit()
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import json
|
||||
import logging
|
||||
import zipfile
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
import defusedxml.ElementTree as ET
|
||||
@@ -732,7 +732,7 @@ async def cancel_queue_item(
|
||||
raise HTTPException(400, f"Cannot cancel item with status '{item.status}'")
|
||||
|
||||
item.status = "cancelled"
|
||||
item.completed_at = datetime.now()
|
||||
item.completed_at = datetime.now(timezone.utc)
|
||||
await db.commit()
|
||||
|
||||
logger.info("Cancelled queue item %s", item_id)
|
||||
@@ -775,7 +775,7 @@ async def stop_queue_item(
|
||||
|
||||
# Update queue item status regardless - if printer is off, print is already stopped
|
||||
item.status = "cancelled"
|
||||
item.completed_at = datetime.now()
|
||||
item.completed_at = datetime.now(timezone.utc)
|
||||
item.error_message = "Stopped by user" if stop_sent else "Stopped by user (printer was offline)"
|
||||
await db.commit()
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""API routes for smart plug management."""
|
||||
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
from fastapi import APIRouter, Body, Depends, HTTPException
|
||||
from pydantic import BaseModel
|
||||
@@ -605,7 +605,7 @@ async def control_smart_plug(
|
||||
elif expected_state == "OFF" and plug.printer_id:
|
||||
# Mark printer offline immediately for faster UI update
|
||||
printer_manager.mark_printer_offline(plug.printer_id)
|
||||
plug.last_checked = datetime.utcnow()
|
||||
plug.last_checked = datetime.now(timezone.utc)
|
||||
await db.commit()
|
||||
|
||||
# Trigger associated scripts if this is a main (non-script) plug
|
||||
@@ -692,7 +692,7 @@ async def get_plug_status(
|
||||
# Update last state in database
|
||||
if is_reachable and data.state:
|
||||
plug.last_state = data.state
|
||||
plug.last_checked = datetime.utcnow()
|
||||
plug.last_checked = datetime.now(timezone.utc)
|
||||
await db.commit()
|
||||
|
||||
energy_data = None
|
||||
@@ -727,7 +727,7 @@ async def get_plug_status(
|
||||
# Update last state in database
|
||||
if status["reachable"]:
|
||||
plug.last_state = status["state"]
|
||||
plug.last_checked = datetime.utcnow()
|
||||
plug.last_checked = datetime.now(timezone.utc)
|
||||
await db.commit()
|
||||
|
||||
# Fetch energy data if device is reachable
|
||||
@@ -756,7 +756,7 @@ async def check_power_alerts(plug: SmartPlug, current_power: float | None, db: A
|
||||
# Cooldown: don't alert more than once per 5 minutes
|
||||
cooldown_minutes = 5
|
||||
if plug.power_alert_last_triggered:
|
||||
time_since_last = datetime.utcnow() - plug.power_alert_last_triggered
|
||||
time_since_last = datetime.now(timezone.utc) - plug.power_alert_last_triggered
|
||||
if time_since_last < timedelta(minutes=cooldown_minutes):
|
||||
return
|
||||
|
||||
@@ -777,7 +777,7 @@ async def check_power_alerts(plug: SmartPlug, current_power: float | None, db: A
|
||||
threshold = plug.power_alert_low
|
||||
|
||||
if alert_triggered:
|
||||
plug.power_alert_last_triggered = datetime.utcnow()
|
||||
plug.power_alert_last_triggered = datetime.now(timezone.utc)
|
||||
await db.commit()
|
||||
|
||||
# Send notification
|
||||
|
||||
Reference in New Issue
Block a user