feat: persist map tile settings (provider/style/API keys) server-side on user record; unbake Thunderforest default key into backend config (THUNDERFOREST_DEFAULT_KEY) served via profile
Build and push images / validate (push) Successful in 2s
Build and push images / build-backend (push) Successful in 7s
Build and push images / build-worker (push) Successful in 6s
Build and push images / build-frontend (push) Successful in 9s

This commit is contained in:
2026-06-23 12:37:27 +01:00
parent ffe285b7ba
commit 3c97595093
8 changed files with 157 additions and 49 deletions
+32 -2
View File
@@ -7,6 +7,7 @@ from datetime import datetime, date, timezone
from app.core.database import get_db
from app.core.security import get_current_user, hash_password, verify_password
from app.core.config import settings
from app.models.user import User, WeightLog
router = APIRouter()
@@ -36,6 +37,8 @@ class ProfileOut(BaseModel):
estimated_max_hr: Optional[int]
is_admin: bool
dashboard_layout: Optional[list] = None
map_settings: Optional[dict] = None
thunderforest_default_key: Optional[str] = None
class Config:
from_attributes = True
@@ -45,6 +48,12 @@ class DashboardLayoutIn(BaseModel):
layout: Optional[list] = None # react-grid-layout array of {i,x,y,w,h}
class MapSettingsIn(BaseModel):
provider: Optional[str] = None
style: Optional[str] = None
keys: Optional[dict] = None # {thunderforest, maptiler, ...} public tile keys
def _estimated_max_hr(user: User) -> Optional[int]:
if user.birth_year:
return 220 - (datetime.now().year - user.birth_year)
@@ -55,7 +64,27 @@ def _estimated_max_hr(user: User) -> Optional[int]:
async def get_profile(current_user: User = Depends(get_current_user)):
return {**{c.name: getattr(current_user, c.name)
for c in User.__table__.columns},
"estimated_max_hr": _estimated_max_hr(current_user)}
"estimated_max_hr": _estimated_max_hr(current_user),
"thunderforest_default_key": settings.thunderforest_default_key}
@router.put("/map-settings")
async def save_map_settings(
body: MapSettingsIn,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""Persist the user's global map tile preference (provider/style/keys)."""
keys = {}
for k, v in (body.keys or {}).items():
keys[str(k)] = (v or "").strip()
current_user.map_settings = {
"provider": body.provider,
"style": body.style,
"keys": keys,
}
await db.commit()
return {"status": "ok"}
@router.put("/dashboard-layout")
@@ -111,7 +140,8 @@ async def update_profile(
return {**{c.name: getattr(current_user, c.name)
for c in User.__table__.columns},
"estimated_max_hr": _estimated_max_hr(current_user)}
"estimated_max_hr": _estimated_max_hr(current_user),
"thunderforest_default_key": settings.thunderforest_default_key}
# ── Password change ────────────────────────────────────────────────────────
+5
View File
@@ -28,6 +28,11 @@ class Settings(BaseSettings):
# The Authorization Callback Domain there must match BASE_URL's host.
strava_client_id: Optional[str] = Field(None, env="STRAVA_CLIENT_ID")
strava_client_secret: Optional[str] = Field(None, env="STRAVA_CLIENT_SECRET")
# Default Thunderforest tile API key, served to clients that haven't set
# their own (a public, client-side tile key). Override per-deployment.
thunderforest_default_key: str = Field(
"872984f587484873a74ea454662ffacb", env="THUNDERFOREST_DEFAULT_KEY"
)
# Files
file_store_path: str = Field("/data/files", env="FILE_STORE_PATH")
# Environment
+9
View File
@@ -118,6 +118,15 @@ async def init_db():
except Exception as e:
print(f"users.dashboard_layout column migration skipped: {e}")
# map_settings column on users added after initial creation
try:
async with engine.begin() as conn:
await conn.execute(text(
"ALTER TABLE users ADD COLUMN IF NOT EXISTS map_settings JSON"
))
except Exception as e:
print(f"users.map_settings column migration skipped: {e}")
# Backfill avg_hr_day / max_hr_day from intraday_hr for Garmin Connect synced days
try:
async with engine.begin() as conn:
+4
View File
@@ -40,6 +40,10 @@ class User(Base):
# Saved dashboard widget layout (react-grid-layout array). Null = use default.
dashboard_layout = Column(JSON, nullable=True)
# Global map tile preference: {provider, style, keys:{thunderforest, maptiler}}.
# Null = use defaults. Tile API keys are public client-side keys.
map_settings = Column(JSON, nullable=True)
activities = relationship("Activity", back_populates="user", cascade="all, delete-orphan")
health_metrics = relationship("HealthMetric", back_populates="user", cascade="all, delete-orphan")
named_routes = relationship("NamedRoute", back_populates="user", cascade="all, delete-orphan")