Batch 1: dashboard, maps, segments rewrite, health, sync UX
Fixes:
- Dashboard: featured most-recent activity card with map + stats
- Maps default to Street; preferCanvas + larger tile buffer for smoother pan/zoom
- Running cadence as colour-banded dots + 165 spm guide line
- Routes: inline row expansion, rename (PATCH /routes/{id}), podium + deltas, tiled map
- Records: remove reversed pace Y-axis
- Profile: remove resting HR; add goal weight
- Health: snapshot weight carry-forward; VO2 trend axis 30-70;
weight goal line + kg/st-lb toggle + axis max; sleep 8h/avg lines
- Garmin sync progress moved to global store with persistent floating bar
Features:
- Speed-coloured activity route (default) with Speed/Solid toggle
- GPS-geometry segments: draw on map, match across all activities,
1st/2nd/3rd leaderboard + podium badges (replaces old distance segments)
- Lap bests: best time per lap across a route + delta column
- Body Battery: highlight activity time windows
Schema: users.goal_weight_kg ALTER; new segments/segment_efforts tables.
Removes RouteSegment, the Segments page, and segment-bests endpoints.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+34
-10
@@ -28,6 +28,7 @@ class User(Base):
|
||||
birth_year = Column(Integer, nullable=True)
|
||||
height_cm = Column(Float, nullable=True)
|
||||
biological_sex = Column(String(8), nullable=True) # 'male' | 'female'
|
||||
goal_weight_kg = Column(Float, nullable=True)
|
||||
|
||||
# PocketID config (stored per-user so admin can set via UI)
|
||||
pocketid_issuer = Column(String(512), nullable=True)
|
||||
@@ -172,22 +173,45 @@ class NamedRoute(Base):
|
||||
|
||||
user = relationship("User", back_populates="named_routes")
|
||||
activities = relationship("Activity", back_populates="named_route")
|
||||
segments = relationship("RouteSegment", back_populates="route", cascade="all, delete-orphan")
|
||||
|
||||
|
||||
class RouteSegment(Base):
|
||||
__tablename__ = "route_segments"
|
||||
class Segment(Base):
|
||||
"""A user-defined GPS segment (a stretch of road/trail) matched across activities."""
|
||||
__tablename__ = "segments"
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
route_id = Column(Integer, ForeignKey("named_routes.id"), nullable=False, index=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=False, index=True)
|
||||
name = Column(String(256), nullable=False)
|
||||
start_distance_m = Column(Float, nullable=False)
|
||||
end_distance_m = Column(Float, nullable=False)
|
||||
description = Column(Text, nullable=True)
|
||||
auto_generated = Column(Boolean, default=False)
|
||||
auto_generated_type = Column(String(20), nullable=True) # '1km' | 'turns' | 'hills'
|
||||
sport_type = Column(String(64), nullable=True)
|
||||
polyline = Column(Text, nullable=True) # encoded GPS geometry of the segment
|
||||
start_lat = Column(Float, nullable=True)
|
||||
start_lng = Column(Float, nullable=True)
|
||||
end_lat = Column(Float, nullable=True)
|
||||
end_lng = Column(Float, nullable=True)
|
||||
distance_m = Column(Float, nullable=True)
|
||||
bounding_box = Column(JSON, nullable=True) # {min_lat,max_lat,min_lon,max_lon}
|
||||
created_from_activity_id = Column(Integer, nullable=True)
|
||||
created_at = Column(DateTime(timezone=True), default=now_utc)
|
||||
|
||||
route = relationship("NamedRoute", back_populates="segments")
|
||||
efforts = relationship("SegmentEffort", back_populates="segment", cascade="all, delete-orphan")
|
||||
|
||||
|
||||
class SegmentEffort(Base):
|
||||
"""One activity's time over a segment."""
|
||||
__tablename__ = "segment_efforts"
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
segment_id = Column(Integer, ForeignKey("segments.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
activity_id = Column(Integer, ForeignKey("activities.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
duration_s = Column(Float, nullable=False)
|
||||
achieved_at = Column(DateTime(timezone=True), nullable=True)
|
||||
rank = Column(Integer, nullable=True) # 1/2/3 for podium, else null
|
||||
|
||||
__table_args__ = (
|
||||
UniqueConstraint("segment_id", "activity_id", name="uq_segment_effort"),
|
||||
)
|
||||
|
||||
segment = relationship("Segment", back_populates="efforts")
|
||||
|
||||
|
||||
class PersonalRecord(Base):
|
||||
|
||||
Reference in New Issue
Block a user