Fix map route not rendering; fix health date filter timezone mismatch
Build and push images / validate (push) Successful in 3s
Build and push images / build-backend (push) Successful in 5s
Build and push images / build-worker (push) Successful in 5s
Build and push images / build-frontend (push) Successful in 9s

This commit is contained in:
2026-06-06 18:52:29 +01:00
parent 24f8417982
commit 93b8f00f94
3 changed files with 73 additions and 50 deletions
+13 -18
View File
@@ -3,7 +3,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, desc, func
from pydantic import BaseModel
from typing import Optional, List
from datetime import datetime, date
from datetime import datetime, timedelta, timezone
from app.core.database import get_db
from app.core.security import get_current_user
@@ -58,12 +58,15 @@ async def list_health_metrics(
current_user: User = Depends(get_current_user),
):
q = select(HealthMetric).where(HealthMetric.user_id == current_user.id)
if from_date:
q = q.where(HealthMetric.date >= from_date)
if to_date:
q = q.where(HealthMetric.date <= to_date)
q = q.order_by(desc(HealthMetric.date)).limit(limit)
if from_date:
from_date_naive = from_date.replace(tzinfo=None) if from_date.tzinfo else from_date
q = q.where(func.date(HealthMetric.date) >= from_date_naive.date())
if to_date:
to_date_naive = to_date.replace(tzinfo=None) if to_date.tzinfo else to_date
q = q.where(func.date(HealthMetric.date) <= to_date_naive.date())
q = q.order_by(desc(HealthMetric.date)).limit(limit)
result = await db.execute(q)
return result.scalars().all()
@@ -73,8 +76,6 @@ async def health_summary(
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""Latest values + 30-day averages for dashboard widgets."""
# Latest record
latest_result = await db.execute(
select(HealthMetric)
.where(HealthMetric.user_id == current_user.id)
@@ -83,9 +84,7 @@ async def health_summary(
)
latest = latest_result.scalar_one_or_none()
# 30-day averages
from datetime import timedelta, timezone
cutoff = datetime.now(timezone.utc) - timedelta(days=30)
cutoff = (datetime.now(timezone.utc) - timedelta(days=30)).date()
avg_result = await db.execute(
select(
func.avg(HealthMetric.resting_hr).label("avg_resting_hr"),
@@ -97,7 +96,7 @@ async def health_summary(
func.avg(HealthMetric.weight_kg).label("avg_weight"),
).where(
HealthMetric.user_id == current_user.id,
HealthMetric.date >= cutoff,
func.date(HealthMetric.date) >= cutoff,
)
)
avgs = avg_result.one()
@@ -122,17 +121,13 @@ async def add_manual_metric(
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""Manually add or update a health metric for a given date."""
from sqlalchemy.dialects.postgresql import insert as pg_insert
from fastapi import HTTPException
date_str = body.get("date")
if not date_str:
from fastapi import HTTPException
raise HTTPException(status_code=400, detail="date required")
metric_date = datetime.fromisoformat(date_str)
# Check for existing
existing = await db.execute(
select(HealthMetric).where(
HealthMetric.user_id == current_user.id,
@@ -153,4 +148,4 @@ async def add_manual_metric(
db.add(metric)
await db.commit()
return {"status": "ok"}
return {"status": "ok"}
@@ -39,55 +39,82 @@ function decodePolyline(encoded) {
return coords
}
function drawRoute(map, polyline, sportType, trackRef) {
if (trackRef.current) {
trackRef.current.remove()
trackRef.current = null
}
if (!polyline) return
const coords = decodePolyline(polyline)
if (!coords.length) return
trackRef.current = L.polyline(coords, {
color: sportColor(sportType),
weight: 3,
opacity: 0.9,
}).addTo(map)
map.fitBounds(trackRef.current.getBounds(), { padding: [20, 20] })
const dot = (color) => L.divIcon({
html: `<div style="width:12px;height:12px;background:${color};border:2px solid white;border-radius:50%"></div>`,
iconSize: [12, 12], iconAnchor: [6, 6], className: '',
})
L.marker(coords[0], { icon: dot('#22c55e') }).addTo(map)
L.marker(coords[coords.length - 1], { icon: dot('#ef4444') }).addTo(map)
}
export default function ActivityMap({ polyline, dataPoints, hoveredDistance, sportType, mapType = 'dark' }) {
const mapRef = useRef(null)
const mapInstanceRef = useRef(null)
const markerRef = useRef(null)
const trackRef = useRef(null)
const tileLayerRef = useRef(null)
const polylineRef = useRef(polyline)
const sportTypeRef = useRef(sportType)
useEffect(() => { polylineRef.current = polyline }, [polyline])
useEffect(() => { sportTypeRef.current = sportType }, [sportType])
useEffect(() => {
if (!mapRef.current || mapInstanceRef.current) return
mapInstanceRef.current = L.map(mapRef.current, { zoomControl: true, attributionControl: true })
const tile = TILE_LAYERS['dark']
tileLayerRef.current = L.tileLayer(tile.url, { attribution: tile.attribution, maxZoom: 19 })
.addTo(mapInstanceRef.current)
return () => { mapInstanceRef.current?.remove(); mapInstanceRef.current = null }
mapInstanceRef.current = L.map(mapRef.current, {
zoomControl: true,
attributionControl: true,
})
const tile = TILE_LAYERS.dark
tileLayerRef.current = L.tileLayer(tile.url, {
attribution: tile.attribution,
maxZoom: 19,
}).addTo(mapInstanceRef.current)
return () => {
mapInstanceRef.current?.remove()
mapInstanceRef.current = null
}
}, [])
// Switch tile layer when mapType changes
useEffect(() => {
if (!mapInstanceRef.current) return
const tile = TILE_LAYERS[mapType] || TILE_LAYERS.dark
if (tileLayerRef.current) {
tileLayerRef.current.remove()
}
tileLayerRef.current = L.tileLayer(tile.url, { attribution: tile.attribution, maxZoom: 19 })
.addTo(mapInstanceRef.current)
if (tileLayerRef.current) tileLayerRef.current.remove()
tileLayerRef.current = L.tileLayer(tile.url, {
attribution: tile.attribution,
maxZoom: 19,
}).addTo(mapInstanceRef.current)
drawRoute(mapInstanceRef.current, polylineRef.current, sportTypeRef.current, trackRef)
}, [mapType])
// Draw route
useEffect(() => {
if (!mapInstanceRef.current || !polyline) return
if (trackRef.current) trackRef.current.remove()
const coords = decodePolyline(polyline)
if (!coords.length) return
trackRef.current = L.polyline(coords, { color: sportColor(sportType), weight: 3, opacity: 0.9 })
.addTo(mapInstanceRef.current)
mapInstanceRef.current.fitBounds(trackRef.current.getBounds(), { padding: [20, 20] })
if (coords.length > 0) {
const dot = (color) => L.divIcon({
html: `<div style="width:12px;height:12px;background:${color};border:2px solid white;border-radius:50%"></div>`,
iconSize: [12, 12], iconAnchor: [6, 6], className: '',
})
L.marker(coords[0], { icon: dot('#22c55e') }).addTo(mapInstanceRef.current)
L.marker(coords[coords.length - 1], { icon: dot('#ef4444') }).addTo(mapInstanceRef.current)
}
if (!mapInstanceRef.current) return
drawRoute(mapInstanceRef.current, polyline, sportType, trackRef)
}, [polyline, sportType])
// Position marker on timeline hover
useEffect(() => {
if (!mapInstanceRef.current || !dataPoints || !hoveredDistance) return
if (!mapInstanceRef.current || !dataPoints || hoveredDistance == null) return
const point = dataPoints.find(p => p.distance_m >= hoveredDistance)
if (!point?.latitude || !point?.longitude) return
if (markerRef.current) {
@@ -97,9 +124,10 @@ export default function ActivityMap({ polyline, dataPoints, hoveredDistance, spo
html: '<div style="width:14px;height:14px;background:#fff;border:3px solid #3b82f6;border-radius:50%;box-shadow:0 0 6px rgba(59,130,246,0.8)"></div>',
iconSize: [14, 14], iconAnchor: [7, 7], className: '',
})
markerRef.current = L.marker([point.latitude, point.longitude], { icon }).addTo(mapInstanceRef.current)
markerRef.current = L.marker([point.latitude, point.longitude], { icon })
.addTo(mapInstanceRef.current)
}
}, [hoveredDistance, dataPoints])
return <div ref={mapRef} style={{ height: '100%', width: '100%', background: '#1a1a2e' }} />
}
}
+1 -1
View File
@@ -79,7 +79,7 @@ function SleepChart({ data }) {
export default function HealthPage() {
const [rangeDays, setRangeDays] = useState(7) // default 1 week
const fromDate = useMemo(() => subDays(new Date(), rangeDays).toISOString(), [rangeDays])
const fromDate = useMemo(() => format(subDays(new Date(), rangeDays), 'yyyy-MM-dd'), [rangeDays])
const { data: summary } = useQuery({
queryKey: ['health-summary'],