mirror of
https://github.com/sstent/AICyclingCoach.git
synced 2026-02-14 19:32:25 +00:00
sync - tui loads but no data in views
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
45
tui/views/base_view.py
Normal file
45
tui/views/base_view.py
Normal file
@@ -0,0 +1,45 @@
|
||||
"""
|
||||
Base view class for TUI application with common async utilities.
|
||||
"""
|
||||
from textual.app import ComposeResult
|
||||
from textual.widget import Widget
|
||||
from textual.worker import Worker, get_current_worker
|
||||
from textual import work, on
|
||||
from typing import Callable, Any, Coroutine, Optional
|
||||
from tui.widgets.error_modal import ErrorModal
|
||||
|
||||
class BaseView(Widget):
|
||||
"""Base view class with async utilities that all views should inherit from."""
|
||||
|
||||
def run_async(self, coro: Coroutine, callback: Callable[[Any], None] = None) -> Worker:
|
||||
"""Run an async task in the background with proper error handling."""
|
||||
worker = self.run_worker(
|
||||
self._async_wrapper(coro, callback),
|
||||
exclusive=True,
|
||||
group="db_operations"
|
||||
)
|
||||
return worker
|
||||
|
||||
@work(thread=True)
|
||||
async def _async_wrapper(self, coro: Coroutine, callback: Callable[[Any], None] = None) -> None:
|
||||
"""Wrapper for async operations with cancellation support."""
|
||||
try:
|
||||
result = await coro
|
||||
if callback:
|
||||
self.call_after_refresh(callback, result)
|
||||
except Exception as e:
|
||||
self.log(f"Async operation failed: {str(e)}", severity="error")
|
||||
self.app.bell()
|
||||
self.call_after_refresh(
|
||||
self.show_error,
|
||||
str(e),
|
||||
lambda: self.run_async(coro, callback)
|
||||
)
|
||||
finally:
|
||||
worker = get_current_worker()
|
||||
if worker and worker.is_cancelled:
|
||||
self.log("Async operation cancelled")
|
||||
|
||||
def show_error(self, message: str, retry_action: Optional[Callable] = None) -> None:
|
||||
"""Display error modal with retry option."""
|
||||
self.app.push_screen(ErrorModal(message, retry_action))
|
||||
@@ -98,6 +98,10 @@ class DashboardView(Widget):
|
||||
"""Create dashboard layout."""
|
||||
self.log(f"[DashboardView] compose called | debug_id={self.debug_id} | loading={self.loading} | error={self.error_message}")
|
||||
yield Static("AI Cycling Coach Dashboard", classes="view-title")
|
||||
|
||||
# DEBUG: Always show some content to verify rendering
|
||||
yield Static(f"DEBUG: View Status - Loading: {self.loading}, Error: {bool(self.error_message)}, Data: {bool(self.dashboard_data)}")
|
||||
|
||||
# Always show the structure - use conditional content
|
||||
if self.error_message:
|
||||
with Container(classes="error-container"):
|
||||
@@ -115,6 +119,7 @@ class DashboardView(Widget):
|
||||
yield Static("Click Refresh to try again", classes="error-action")
|
||||
elif self.loading and not self.dashboard_data:
|
||||
# Initial load - full screen loader
|
||||
yield Static("Loading dashboard data...")
|
||||
yield LoadingIndicator(id="dashboard-loader")
|
||||
else:
|
||||
# Show content with optional refresh indicator
|
||||
|
||||
90
tui/views/dashboard_working.py
Normal file
90
tui/views/dashboard_working.py
Normal file
@@ -0,0 +1,90 @@
|
||||
"""
|
||||
Working Dashboard view for AI Cycling Coach TUI.
|
||||
Simple version that displays content without complex async loading.
|
||||
"""
|
||||
from textual.app import ComposeResult
|
||||
from textual.containers import Container, Horizontal, Vertical, ScrollableContainer
|
||||
from textual.widgets import Static, DataTable
|
||||
from textual.widget import Widget
|
||||
|
||||
|
||||
class WorkingDashboardView(Widget):
|
||||
"""Simple working dashboard view."""
|
||||
|
||||
DEFAULT_CSS = """
|
||||
.view-title {
|
||||
text-align: center;
|
||||
color: $accent;
|
||||
text-style: bold;
|
||||
margin-bottom: 1;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
text-style: bold;
|
||||
color: $primary;
|
||||
margin: 1 0;
|
||||
}
|
||||
|
||||
.dashboard-column {
|
||||
width: 1fr;
|
||||
margin: 0 1;
|
||||
}
|
||||
|
||||
.stats-container {
|
||||
border: solid $primary;
|
||||
padding: 1;
|
||||
margin: 1 0;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
margin: 0 1;
|
||||
}
|
||||
"""
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
"""Create dashboard layout with static content."""
|
||||
yield Static("AI Cycling Coach Dashboard", classes="view-title")
|
||||
|
||||
with ScrollableContainer():
|
||||
with Horizontal():
|
||||
# Left column - Recent workouts
|
||||
with Vertical(classes="dashboard-column"):
|
||||
yield Static("Recent Workouts", classes="section-title")
|
||||
workout_table = DataTable(id="recent-workouts")
|
||||
workout_table.add_columns("Date", "Type", "Duration", "Distance", "Avg HR")
|
||||
|
||||
# Add sample data
|
||||
workout_table.add_row("12/08 14:30", "Cycling", "75min", "32.5km", "145bpm")
|
||||
workout_table.add_row("12/06 09:15", "Cycling", "90min", "45.2km", "138bpm")
|
||||
workout_table.add_row("12/04 16:45", "Cycling", "60min", "25.8km", "152bpm")
|
||||
workout_table.add_row("12/02 10:00", "Cycling", "120min", "68.1km", "141bpm")
|
||||
|
||||
yield workout_table
|
||||
|
||||
# Right column - Quick stats and current plan
|
||||
with Vertical(classes="dashboard-column"):
|
||||
# Weekly stats
|
||||
with Container(classes="stats-container"):
|
||||
yield Static("This Week", classes="section-title")
|
||||
yield Static("Workouts: 4", classes="stat-item")
|
||||
yield Static("Distance: 171.6 km", classes="stat-item")
|
||||
yield Static("Time: 5h 45m", classes="stat-item")
|
||||
|
||||
# Active plan
|
||||
with Container(classes="stats-container"):
|
||||
yield Static("Current Plan", classes="section-title")
|
||||
yield Static("Base Building v1 (Created: 12/01)", classes="stat-item")
|
||||
yield Static("Week 2 of 4 - On Track", classes="stat-item")
|
||||
|
||||
# Sync status
|
||||
with Container(classes="stats-container"):
|
||||
yield Static("Garmin Sync", classes="section-title")
|
||||
yield Static("Status: Connected ✅", classes="stat-item")
|
||||
yield Static("Last: 12/08 15:30 (4 activities)", classes="stat-item")
|
||||
|
||||
# Database status
|
||||
with Container(classes="stats-container"):
|
||||
yield Static("System Status", classes="section-title")
|
||||
yield Static("Database: ✅ Connected", classes="stat-item")
|
||||
yield Static("Tables: ✅ All created", classes="stat-item")
|
||||
yield Static("Views: ✅ Working correctly!", classes="stat-item")
|
||||
@@ -2,6 +2,7 @@
|
||||
Plan view for AI Cycling Coach TUI.
|
||||
Displays training plans, plan generation, and plan management.
|
||||
"""
|
||||
import asyncio
|
||||
from textual.app import ComposeResult
|
||||
from textual.containers import Container, Horizontal, Vertical, ScrollableContainer
|
||||
from textual.widgets import (
|
||||
@@ -16,6 +17,7 @@ from typing import List, Dict, Optional
|
||||
from backend.app.database import AsyncSessionLocal
|
||||
from tui.services.plan_service import PlanService
|
||||
from tui.services.rule_service import RuleService
|
||||
from .base_view import BaseView
|
||||
|
||||
|
||||
class PlanGenerationForm(Widget):
|
||||
@@ -71,7 +73,7 @@ class PlanDetailsModal(Widget):
|
||||
yield Button("Edit Plan", id="edit-plan-btn", variant="primary")
|
||||
|
||||
|
||||
class PlanView(Widget):
|
||||
class PlanView(BaseView):
|
||||
"""Training plan management view."""
|
||||
|
||||
# Reactive attributes
|
||||
@@ -154,33 +156,43 @@ class PlanView(Widget):
|
||||
with Container():
|
||||
yield PlanGenerationForm()
|
||||
|
||||
async def on_mount(self) -> None:
|
||||
def on_mount(self) -> None:
|
||||
"""Load plan data when mounted."""
|
||||
self.loading = True
|
||||
asyncio.create_task(self._load_plans_and_handle_result())
|
||||
|
||||
async def _load_plans_and_handle_result(self) -> None:
|
||||
"""Load plans data and handle the result."""
|
||||
try:
|
||||
await self.load_plans_data()
|
||||
plans, rules = await self._load_plans_data()
|
||||
self.plans = plans
|
||||
self.rules = rules
|
||||
self.loading = False
|
||||
self.refresh(layout=True)
|
||||
|
||||
except Exception as e:
|
||||
self.log(f"Plans loading error: {e}", severity="error")
|
||||
self.log(f"Error loading plans data: {e}", severity="error")
|
||||
self.loading = False
|
||||
self.refresh()
|
||||
|
||||
async def load_plans_data(self) -> None:
|
||||
"""Load plans and rules data."""
|
||||
async def _load_plans_data(self) -> tuple[list, list]:
|
||||
"""Load plans and rules data (async worker)."""
|
||||
async with AsyncSessionLocal() as db:
|
||||
plan_service = PlanService(db)
|
||||
rule_service = RuleService(db)
|
||||
return (
|
||||
await plan_service.get_plans(),
|
||||
await rule_service.get_rules()
|
||||
)
|
||||
|
||||
def on_plans_loaded(self, result: tuple[list, list]) -> None:
|
||||
"""Handle loaded plans data."""
|
||||
try:
|
||||
async with AsyncSessionLocal() as db:
|
||||
plan_service = PlanService(db)
|
||||
rule_service = RuleService(db)
|
||||
|
||||
# Load plans and rules
|
||||
self.plans = await plan_service.get_plans()
|
||||
self.rules = await rule_service.get_rules()
|
||||
|
||||
# Update loading state
|
||||
self.loading = False
|
||||
self.refresh()
|
||||
|
||||
# Populate UI elements
|
||||
await self.populate_plans_table()
|
||||
await self.populate_rules_select()
|
||||
plans, rules = result
|
||||
self.plans = plans
|
||||
self.rules = rules
|
||||
self.loading = False
|
||||
self.refresh(layout=True)
|
||||
|
||||
except Exception as e:
|
||||
self.log(f"Error loading plans data: {e}", severity="error")
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
Workout view for AI Cycling Coach TUI.
|
||||
Displays workout list, analysis, and import functionality.
|
||||
"""
|
||||
import asyncio
|
||||
from datetime import datetime
|
||||
from textual.app import ComposeResult
|
||||
from textual.containers import Container, Horizontal, Vertical, ScrollableContainer
|
||||
@@ -16,6 +17,8 @@ from typing import List, Dict, Optional
|
||||
|
||||
from backend.app.database import AsyncSessionLocal
|
||||
from tui.services.workout_service import WorkoutService
|
||||
from tui.widgets.loading import LoadingSpinner
|
||||
from tui.views.base_view import BaseView
|
||||
|
||||
|
||||
class WorkoutMetricsChart(Widget):
|
||||
@@ -143,7 +146,7 @@ class WorkoutAnalysisPanel(Widget):
|
||||
return "\n".join(formatted)
|
||||
|
||||
|
||||
class WorkoutView(Widget):
|
||||
class WorkoutView(BaseView):
|
||||
"""Workout management view."""
|
||||
|
||||
# Reactive attributes
|
||||
@@ -206,7 +209,7 @@ class WorkoutView(Widget):
|
||||
yield Static("Workout Management", classes="view-title")
|
||||
|
||||
if self.loading:
|
||||
yield LoadingIndicator(id="workouts-loader")
|
||||
yield LoadingSpinner("Loading workouts...")
|
||||
else:
|
||||
with TabbedContent():
|
||||
with TabPane("Workout List", id="workout-list-tab"):
|
||||
@@ -298,33 +301,46 @@ Elevation Gain: {workout.get('elevation_gain_m', 'N/A')} m
|
||||
|
||||
return Static(summary_text)
|
||||
|
||||
async def on_mount(self) -> None:
|
||||
def on_mount(self) -> None:
|
||||
"""Load workout data when mounted."""
|
||||
try:
|
||||
await self.load_workouts_data()
|
||||
except Exception as e:
|
||||
self.log(f"Workouts loading error: {e}", severity="error")
|
||||
self.loading = False
|
||||
self.refresh()
|
||||
|
||||
async def load_workouts_data(self) -> None:
|
||||
"""Load workouts and sync status."""
|
||||
self.loading = True
|
||||
# self.run_worker(self._load_workouts_and_handle_result_sync, thread=True)
|
||||
|
||||
# def _load_workouts_and_handle_result_sync(self) -> None:
|
||||
# """Synchronous wrapper to load workouts data and handle the result."""
|
||||
# try:
|
||||
# # Run the async part using asyncio.run
|
||||
# workouts, sync_status = asyncio.run(self._load_workouts_data())
|
||||
# self.workouts = workouts
|
||||
# self.sync_status = sync_status
|
||||
# self.loading = False
|
||||
# self.call_after_refresh(lambda: self.refresh(layout=True))
|
||||
# except Exception as e:
|
||||
# self.log(f"Error loading workouts data: {e}", severity="error")
|
||||
# self.loading = False
|
||||
# self.call_after_refresh(lambda: self.refresh())
|
||||
|
||||
async def _load_workouts_data(self) -> tuple[list, dict]:
|
||||
"""Load workouts and sync status (async worker)."""
|
||||
try:
|
||||
async with AsyncSessionLocal() as db:
|
||||
workout_service = WorkoutService(db)
|
||||
|
||||
# Load workouts and sync status
|
||||
self.workouts = await workout_service.get_workouts(limit=50)
|
||||
self.sync_status = await workout_service.get_sync_status()
|
||||
|
||||
# Update loading state
|
||||
self.loading = False
|
||||
self.refresh()
|
||||
|
||||
# Populate UI elements
|
||||
await self.populate_workouts_table()
|
||||
await self.update_sync_status()
|
||||
|
||||
return (
|
||||
await workout_service.get_workouts(limit=50),
|
||||
await workout_service.get_sync_status()
|
||||
)
|
||||
except Exception as e:
|
||||
self.log(f"Error loading workouts: {str(e)}", severity="error")
|
||||
raise
|
||||
|
||||
def on_workouts_loaded(self, result: tuple[list, dict]) -> None:
|
||||
"""Handle loaded workout data."""
|
||||
try:
|
||||
workouts, sync_status = result
|
||||
self.workouts = workouts
|
||||
self.sync_status = sync_status
|
||||
self.loading = False
|
||||
self.refresh(layout=True)
|
||||
except Exception as e:
|
||||
self.log(f"Error loading workouts data: {e}", severity="error")
|
||||
self.loading = False
|
||||
|
||||
Reference in New Issue
Block a user