sync - tui loads but no data in views

This commit is contained in:
2025-09-26 08:33:02 -07:00
parent 6d0d8493aa
commit 5c0e05db16
27 changed files with 283 additions and 2797 deletions

45
tui/views/base_view.py Normal file
View 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))

View File

@@ -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

View 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")

View File

@@ -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")

View File

@@ -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