Files
bambuddy/backend/app/api/routes/ams_history.py
T
maziggy ed36eafbec 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.
2026-02-24 08:18:02 +01:00

134 lines
4.3 KiB
Python

"""API routes for AMS sensor history."""
from datetime import datetime, timedelta, timezone
from fastapi import APIRouter, Depends, Query
from pydantic import BaseModel
from sqlalchemy import and_, func, select
from sqlalchemy.ext.asyncio import AsyncSession
from backend.app.core.auth import RequirePermissionIfAuthEnabled
from backend.app.core.database import get_db
from backend.app.core.permissions import Permission
from backend.app.models.ams_history import AMSSensorHistory
from backend.app.models.user import User
router = APIRouter(prefix="/ams-history", tags=["ams-history"])
class AMSHistoryPoint(BaseModel):
recorded_at: datetime
humidity: float | None
humidity_raw: float | None
temperature: float | None
class AMSHistoryResponse(BaseModel):
printer_id: int
ams_id: int
data: list[AMSHistoryPoint]
min_humidity: float | None
max_humidity: float | None
avg_humidity: float | None
min_temperature: float | None
max_temperature: float | None
avg_temperature: float | None
@router.get("/{printer_id}/{ams_id}", response_model=AMSHistoryResponse)
async def get_ams_history(
printer_id: int,
ams_id: int,
hours: int = Query(default=24, ge=1, le=168, description="Hours of history (1-168)"),
db: AsyncSession = Depends(get_db),
_: User | None = RequirePermissionIfAuthEnabled(Permission.AMS_HISTORY_READ),
):
"""Get AMS sensor history for a specific printer and AMS unit."""
since = datetime.now(timezone.utc) - timedelta(hours=hours)
# Get data points
result = await db.execute(
select(AMSSensorHistory)
.where(
and_(
AMSSensorHistory.printer_id == printer_id,
AMSSensorHistory.ams_id == ams_id,
AMSSensorHistory.recorded_at >= since,
)
)
.order_by(AMSSensorHistory.recorded_at)
)
records = result.scalars().all()
# Calculate stats
stats_result = await db.execute(
select(
func.min(AMSSensorHistory.humidity).label("min_humidity"),
func.max(AMSSensorHistory.humidity).label("max_humidity"),
func.avg(AMSSensorHistory.humidity).label("avg_humidity"),
func.min(AMSSensorHistory.temperature).label("min_temp"),
func.max(AMSSensorHistory.temperature).label("max_temp"),
func.avg(AMSSensorHistory.temperature).label("avg_temp"),
).where(
and_(
AMSSensorHistory.printer_id == printer_id,
AMSSensorHistory.ams_id == ams_id,
AMSSensorHistory.recorded_at >= since,
)
)
)
stats = stats_result.one()
return AMSHistoryResponse(
printer_id=printer_id,
ams_id=ams_id,
data=[
AMSHistoryPoint(
recorded_at=r.recorded_at,
humidity=r.humidity,
humidity_raw=r.humidity_raw,
temperature=r.temperature,
)
for r in records
],
min_humidity=stats.min_humidity,
max_humidity=stats.max_humidity,
avg_humidity=round(stats.avg_humidity, 1) if stats.avg_humidity else None,
min_temperature=stats.min_temp,
max_temperature=stats.max_temp,
avg_temperature=round(stats.avg_temp, 1) if stats.avg_temp else None,
)
@router.delete("/{printer_id}")
async def delete_old_history(
printer_id: int,
days: int = Query(default=30, ge=1, le=365, description="Delete data older than X days"),
db: AsyncSession = Depends(get_db),
_: User | None = RequirePermissionIfAuthEnabled(Permission.AMS_HISTORY_READ),
):
"""Delete old AMS history data for a printer."""
cutoff = datetime.now(timezone.utc) - timedelta(days=days)
result = await db.execute(
select(func.count(AMSSensorHistory.id)).where(
and_(
AMSSensorHistory.printer_id == printer_id,
AMSSensorHistory.recorded_at < cutoff,
)
)
)
count = result.scalar()
await db.execute(
AMSSensorHistory.__table__.delete().where(
and_(
AMSSensorHistory.printer_id == printer_id,
AMSSensorHistory.recorded_at < cutoff,
)
)
)
await db.commit()
return {"deleted": count, "message": f"Deleted {count} records older than {days} days"}