change to TUI

This commit is contained in:
2025-09-12 09:08:10 -07:00
parent 7c7dcb5b10
commit e0e70f6508
165 changed files with 3438 additions and 16154 deletions

Binary file not shown.

Binary file not shown.

View File

@@ -1,11 +1,20 @@
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
DATABASE_URL: str
GPX_STORAGE_PATH: str
AI_MODEL: str = "openrouter/auto"
API_KEY: str
# Database settings
DATABASE_URL: str = "sqlite+aiosqlite:///data/cycling_coach.db"
model_config = SettingsConfigDict(env_file=".env", extra="ignore")
# File storage settings
GPX_STORAGE_PATH: str = "data/gpx"
# AI settings
AI_MODEL: str = "deepseek/deepseek-r1"
OPENROUTER_API_KEY: str = ""
# Garmin settings
GARMIN_USERNAME: str = ""
GARMIN_PASSWORD: str = ""
model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8", extra="ignore")
settings = Settings()

View File

@@ -1,10 +1,19 @@
import os
from pathlib import Path
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import declarative_base, sessionmaker
DATABASE_URL = os.getenv("DATABASE_URL", "postgresql+asyncpg://postgres:password@db:5432/cycling")
# Use SQLite database in data directory
DATA_DIR = Path("data")
DATABASE_PATH = DATA_DIR / "cycling_coach.db"
DATABASE_URL = os.getenv("DATABASE_URL", f"sqlite+aiosqlite:///{DATABASE_PATH}")
engine = create_async_engine(
DATABASE_URL,
echo=False, # Set to True for SQL debugging
connect_args={"check_same_thread": False} if "sqlite" in DATABASE_URL else {}
)
engine = create_async_engine(DATABASE_URL, echo=True)
AsyncSessionLocal = sessionmaker(
bind=engine,
class_=AsyncSession,
@@ -15,4 +24,19 @@ Base = declarative_base()
async def get_db() -> AsyncSession:
async with AsyncSessionLocal() as session:
yield session
yield session
async def init_db():
"""Initialize the database by creating all tables."""
# Ensure data directory exists
DATA_DIR.mkdir(exist_ok=True)
# Import all models to ensure they are registered
from .models import (
user, rule, plan, plan_rule, workout,
analysis, route, section, garmin_sync_log, prompt
)
# Create all tables
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)

View File

@@ -1,7 +1,7 @@
from fastapi import Depends
from sqlalchemy.ext.asyncio import AsyncSession
from app.database import get_db
from app.services.ai_service import AIService
from backend.app.database import get_db
from backend.app.services.ai_service import AIService
from typing import AsyncGenerator

View File

@@ -3,7 +3,7 @@ from .route import Route
from .section import Section
from .rule import Rule
from .plan import Plan
from .plan_rule import PlanRule
from .plan_rule import plan_rules
from .user import User
from .workout import Workout
from .analysis import Analysis

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,4 +1,5 @@
from sqlalchemy import Column, Integer, String, ForeignKey, JSON, Boolean, DateTime, func
from datetime import datetime
from sqlalchemy import Column, Integer, String, ForeignKey, JSON, Boolean, DateTime
from sqlalchemy.orm import relationship
from .base import BaseModel
@@ -13,7 +14,7 @@ class Analysis(BaseModel):
suggestions = Column(JSON)
approved = Column(Boolean, default=False)
created_plan_id = Column(Integer, ForeignKey('plans.id'))
approved_at = Column(DateTime(timezone=True), server_default=func.now())
approved_at = Column(DateTime, default=datetime.utcnow) # Changed from server_default=func.now()
# Relationships
workout = relationship("Workout", back_populates="analyses")

View File

@@ -1,15 +1,13 @@
from datetime import datetime
from uuid import UUID, uuid4
from sqlalchemy import Column, DateTime
from sqlalchemy import Column, Integer, DateTime
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.dialects.postgresql import UUID as PG_UUID
Base = declarative_base()
class BaseModel(Base):
__abstract__ = True
id = Column(PG_UUID(as_uuid=True), primary_key=True, default=uuid4)
id = Column(Integer, primary_key=True, autoincrement=True)
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)

View File

@@ -1,12 +1,11 @@
from sqlalchemy import Column, Integer, ForeignKey
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy import Column, Integer, ForeignKey, JSON
from sqlalchemy.orm import relationship
from .base import BaseModel
class Plan(BaseModel):
__tablename__ = "plans"
jsonb_plan = Column(JSONB, nullable=False)
jsonb_plan = Column(JSON, nullable=False) # Changed from JSONB to JSON for SQLite compatibility
version = Column(Integer, nullable=False)
parent_plan_id = Column(Integer, ForeignKey('plans.id'), nullable=True)

View File

@@ -1,12 +1,9 @@
from sqlalchemy import Column, Integer, ForeignKey
from sqlalchemy.orm import relationship
from .base import BaseModel
from sqlalchemy import Column, Integer, ForeignKey, Table
from .base import Base
class PlanRule(BaseModel):
__tablename__ = "plan_rules"
plan_id = Column(Integer, ForeignKey('plans.id'), primary_key=True)
rule_id = Column(Integer, ForeignKey('rules.id'), primary_key=True)
plan = relationship("Plan", back_populates="rules")
rule = relationship("Rule", back_populates="plans")
# Association table for many-to-many relationship between plans and rules
plan_rules = Table(
'plan_rules', Base.metadata,
Column('plan_id', Integer, ForeignKey('plans.id'), primary_key=True),
Column('rule_id', Integer, ForeignKey('rules.id'), primary_key=True)
)

View File

@@ -1,7 +1,12 @@
from .base import BaseModel
from sqlalchemy import Column, String
from sqlalchemy.orm import relationship
from .base import BaseModel
class User(BaseModel):
__tablename__ = "users"
plans = relationship("Plan", back_populates="user")
username = Column(String(100), nullable=False, unique=True)
email = Column(String(255), nullable=True)
# Note: Relationship removed as Plan model doesn't have user_id field
# plans = relationship("Plan", back_populates="user")

View File

@@ -1,9 +1,9 @@
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from app.database import get_db
from app.models.workout import Workout
from app.models.plan import Plan
from app.models.garmin_sync_log import GarminSyncLog
from backend.app.database import get_db
from backend.app.models.workout import Workout
from backend.app.models.plan import Plan
from backend.app.models.garmin_sync_log import GarminSyncLog
from sqlalchemy import select, desc
from datetime import datetime, timedelta

View File

@@ -1,6 +1,6 @@
from fastapi import APIRouter, Query, HTTPException
from fastapi.responses import FileResponse
from app.services.export_service import ExportService
from backend.app.services.export_service import ExportService
from pathlib import Path
import logging

View File

@@ -1,8 +1,8 @@
from fastapi import APIRouter, Depends, BackgroundTasks
from sqlalchemy.ext.asyncio import AsyncSession
from app.dependencies import verify_api_key
from app.services.workout_sync import WorkoutSyncService
from app.database import get_db
from backend.app.dependencies import verify_api_key
from backend.app.services.workout_sync import WorkoutSyncService
from backend.app.database import get_db
router = APIRouter(dependencies=[Depends(verify_api_key)])

View File

@@ -1,9 +1,9 @@
from fastapi import APIRouter, UploadFile, File, Depends, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from app.database import get_db
from app.services.gpx import parse_gpx, store_gpx_file
from app.schemas.gpx import RouteCreate, Route as RouteSchema
from app.models import Route
from backend.app.database import get_db
from backend.app.services.gpx import parse_gpx, store_gpx_file
from backend.app.schemas.gpx import RouteCreate, Route as RouteSchema
from backend.app.models import Route
import os
router = APIRouter(prefix="/gpx", tags=["GPX Routes"])

View File

@@ -1,6 +1,6 @@
from fastapi import APIRouter
from fastapi.responses import PlainTextResponse, JSONResponse
from app.services.health_monitor import HealthMonitor
from backend.app.services.health_monitor import HealthMonitor
from prometheus_client import generate_latest, CONTENT_TYPE_LATEST, Gauge
from pathlib import Path
import json

View File

@@ -1,6 +1,6 @@
from fastapi import APIRouter, UploadFile, File, Form, HTTPException
from fastapi.responses import JSONResponse
from app.services.import_service import ImportService
from backend.app.services.import_service import ImportService
import logging
from typing import Optional

View File

@@ -1,12 +1,12 @@
from fastapi import APIRouter, Depends, HTTPException, BackgroundTasks
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from app.database import get_db
from app.models.plan import Plan as PlanModel
from app.models.rule import Rule
from app.schemas.plan import PlanCreate, Plan as PlanSchema, PlanGenerationRequest, PlanGenerationResponse
from app.dependencies import get_ai_service
from app.services.ai_service import AIService
from backend.app.database import get_db
from backend.app.models.plan import Plan as PlanModel
from backend.app.models.rule import Rule
from backend.app.schemas.plan import PlanCreate, Plan as PlanSchema, PlanGenerationRequest, PlanGenerationResponse
from backend.app.dependencies import get_ai_service
from backend.app.services.ai_service import AIService
from uuid import UUID, uuid4
from datetime import datetime
from typing import List

View File

@@ -3,10 +3,10 @@ from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from typing import List
from app.database import get_db
from app.models.prompt import Prompt
from app.schemas.prompt import Prompt as PromptSchema, PromptCreate, PromptUpdate
from app.services.prompt_manager import PromptManager
from backend.app.database import get_db
from backend.app.models.prompt import Prompt
from backend.app.schemas.prompt import Prompt as PromptSchema, PromptCreate, PromptUpdate
from backend.app.services.prompt_manager import PromptManager
router = APIRouter()

View File

@@ -1,11 +1,11 @@
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from app.database import get_db
from app.models.rule import Rule
from app.schemas.rule import RuleCreate, Rule as RuleSchema, NaturalLanguageRuleRequest, ParsedRuleResponse
from app.dependencies import get_ai_service
from app.services.ai_service import AIService
from backend.app.database import get_db
from backend.app.models.rule import Rule
from backend.app.schemas.rule import RuleCreate, Rule as RuleSchema, NaturalLanguageRuleRequest, ParsedRuleResponse
from backend.app.dependencies import get_ai_service
from backend.app.services.ai_service import AIService
from uuid import UUID
from typing import List

View File

@@ -3,17 +3,17 @@ from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from typing import List
from app.database import get_db
from app.models.workout import Workout
from app.models.analysis import Analysis
from app.models.garmin_sync_log import GarminSyncLog
from app.models.plan import Plan
from app.schemas.workout import Workout as WorkoutSchema, WorkoutSyncStatus, WorkoutMetric
from app.schemas.analysis import Analysis as AnalysisSchema
from app.schemas.plan import Plan as PlanSchema
from app.services.workout_sync import WorkoutSyncService
from app.services.ai_service import AIService
from app.services.plan_evolution import PlanEvolutionService
from backend.app.database import get_db
from backend.app.models.workout import Workout
from backend.app.models.analysis import Analysis
from backend.app.models.garmin_sync_log import GarminSyncLog
from backend.app.models.plan import Plan
from backend.app.schemas.workout import Workout as WorkoutSchema, WorkoutSyncStatus, WorkoutMetric
from backend.app.schemas.analysis import Analysis as AnalysisSchema
from backend.app.schemas.plan import Plan as PlanSchema
from backend.app.services.workout_sync import WorkoutSyncService
from backend.app.services.ai_service import AIService
from backend.app.services.plan_evolution import PlanEvolutionService
router = APIRouter()

View File

@@ -3,8 +3,8 @@ import asyncio
from typing import Dict, Any, List, Optional
import httpx
import json
from app.services.prompt_manager import PromptManager
from app.models.workout import Workout
from backend.app.services.prompt_manager import PromptManager
from backend.app.models.workout import Workout
import logging
logger = logging.getLogger(__name__)

View File

@@ -2,8 +2,8 @@ import json
from pathlib import Path
from datetime import datetime
import zipfile
from app.database import SessionLocal
from app.models import Route, Rule, Plan
from backend.app.database import SessionLocal
from backend.app.models import Route, Rule, Plan
import tempfile
import logging
import shutil

View File

@@ -3,7 +3,7 @@ import uuid
import logging
from fastapi import UploadFile, HTTPException
import gpxpy
from app.config import settings
from backend.app.config import settings
logger = logging.getLogger(__name__)

View File

@@ -3,10 +3,10 @@ from datetime import datetime
import logging
from typing import Dict, Any
from sqlalchemy import text
from app.database import get_db
from app.models.garmin_sync_log import GarminSyncLog, SyncStatus
from backend.app.database import get_db
from backend.app.models.garmin_sync_log import GarminSyncLog, SyncStatus
import requests
from app.config import settings
from backend.app.config import settings
logger = logging.getLogger(__name__)
@@ -43,12 +43,12 @@ class HealthMonitor:
def _get_sync_queue_size(self) -> int:
"""Get number of pending sync operations"""
from app.models.garmin_sync_log import GarminSyncLog, SyncStatus
from backend.app.models.garmin_sync_log import GarminSyncLog, SyncStatus
return GarminSyncLog.query.filter_by(status=SyncStatus.PENDING).count()
def _count_pending_analyses(self) -> int:
"""Count workouts needing analysis"""
from app.models.workout import Workout
from backend.app.models.workout import Workout
return Workout.query.filter_by(analysis_status='pending').count()
def _check_database(self) -> str:

View File

@@ -3,8 +3,8 @@ import zipfile
from pathlib import Path
import tempfile
from datetime import datetime
from app.database import SessionLocal
from app.models import Route, Rule, Plan
from backend.app.database import SessionLocal
from backend.app.models import Route, Rule, Plan
import shutil
import logging
from sqlalchemy import and_

View File

@@ -1,8 +1,8 @@
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from app.services.ai_service import AIService
from app.models.analysis import Analysis
from app.models.plan import Plan
from backend.app.services.ai_service import AIService
from backend.app.models.analysis import Analysis
from backend.app.models.plan import Plan
import logging
logger = logging.getLogger(__name__)

View File

@@ -1,6 +1,6 @@
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, update, func
from app.models.prompt import Prompt
from backend.app.models.prompt import Prompt
import logging
logger = logging.getLogger(__name__)

View File

@@ -1,9 +1,9 @@
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, desc
from app.services.garmin import GarminService, GarminAPIError, GarminAuthError
from app.models.workout import Workout
from app.models.garmin_sync_log import GarminSyncLog
from app.models.garmin_sync_log import GarminSyncLog
from backend.app.services.garmin import GarminService, GarminAPIError, GarminAuthError
from backend.app.models.workout import Workout
from backend.app.models.garmin_sync_log import GarminSyncLog
from backend.app.models.garmin_sync_log import GarminSyncLog
from datetime import datetime, timedelta
import logging
from typing import Dict, Any