mirror of
https://github.com/sstent/aicyclingcoach-go.git
synced 2026-01-26 00:51:56 +00:00
sync
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/sstent/fitness-tui/internal/config"
|
||||
@@ -82,10 +83,19 @@ func runTUI() {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Initialize file logger
|
||||
logPath := filepath.Join(cfg.StoragePath, "fitness-tui.log")
|
||||
fileLogger, err := garmin.NewFileLogger(logPath)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to initialize logger: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer fileLogger.Close()
|
||||
|
||||
activityStorage := storage.NewActivityStorage(cfg.StoragePath)
|
||||
garminClient := garmin.NewClient(cfg.Garmin.Username, cfg.Garmin.Password, cfg.StoragePath)
|
||||
|
||||
app := tui.NewApp(activityStorage, garminClient)
|
||||
app := tui.NewApp(activityStorage, garminClient, fileLogger)
|
||||
if err := app.Run(); err != nil {
|
||||
fmt.Printf("Application error: %v\n", err)
|
||||
os.Exit(1)
|
||||
|
||||
Binary file not shown.
@@ -98,7 +98,8 @@ func (c *Client) GetActivities(ctx context.Context, limit int, logger Logger) ([
|
||||
continue
|
||||
}
|
||||
|
||||
activities = append(activities, &models.Activity{
|
||||
// Convert garth activity to internal model
|
||||
activity := &models.Activity{
|
||||
ID: fmt.Sprintf("%d", ga.ActivityID),
|
||||
Name: ga.ActivityName,
|
||||
Type: ga.ActivityType.TypeKey,
|
||||
@@ -107,7 +108,31 @@ func (c *Client) GetActivities(ctx context.Context, limit int, logger Logger) ([
|
||||
Duration: time.Duration(ga.Duration) * time.Second,
|
||||
Elevation: ga.ElevationGain,
|
||||
Calories: int(ga.Calories),
|
||||
})
|
||||
}
|
||||
|
||||
// Populate metrics from garth data
|
||||
if ga.AverageHR > 0 {
|
||||
activity.Metrics.AvgHeartRate = int(ga.AverageHR)
|
||||
}
|
||||
if ga.MaxHR > 0 {
|
||||
activity.Metrics.MaxHeartRate = int(ga.MaxHR)
|
||||
}
|
||||
if ga.AverageSpeed > 0 {
|
||||
// Convert m/s to km/h
|
||||
activity.Metrics.AvgSpeed = ga.AverageSpeed * 3.6
|
||||
}
|
||||
if ga.ElevationGain > 0 {
|
||||
activity.Metrics.ElevationGain = ga.ElevationGain
|
||||
}
|
||||
if ga.ElevationLoss > 0 {
|
||||
activity.Metrics.ElevationLoss = ga.ElevationLoss
|
||||
}
|
||||
if ga.AverageSpeed > 0 && ga.Distance > 0 {
|
||||
// Calculate pace: seconds per km
|
||||
activity.Metrics.AvgPace = (ga.Duration / ga.Distance) * 1000
|
||||
}
|
||||
|
||||
activities = append(activities, activity)
|
||||
}
|
||||
|
||||
logger.Infof("Successfully fetched %d activities", len(activities))
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
package garmin
|
||||
|
||||
import "fmt"
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// Logger defines the interface for logging in Garmin operations
|
||||
type Logger interface {
|
||||
@@ -36,3 +41,48 @@ func (l *NoopLogger) Debugf(format string, args ...interface{}) {}
|
||||
func (l *NoopLogger) Infof(format string, args ...interface{}) {}
|
||||
func (l *NoopLogger) Warnf(format string, args ...interface{}) {}
|
||||
func (l *NoopLogger) Errorf(format string, args ...interface{}) {}
|
||||
|
||||
// FileLogger implements Logger that writes to a file
|
||||
type FileLogger struct {
|
||||
logger *log.Logger
|
||||
file *os.File
|
||||
}
|
||||
|
||||
func NewFileLogger(logPath string) (*FileLogger, error) {
|
||||
// Create log directory if it doesn't exist
|
||||
dir := filepath.Dir(logPath)
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return nil, fmt.Errorf("failed to create log directory: %w", err)
|
||||
}
|
||||
|
||||
file, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open log file: %w", err)
|
||||
}
|
||||
|
||||
logger := log.New(file, "", log.LstdFlags)
|
||||
return &FileLogger{
|
||||
logger: logger,
|
||||
file: file,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (l *FileLogger) Debugf(format string, args ...interface{}) {
|
||||
l.logger.Printf("[DEBUG] "+format, args...)
|
||||
}
|
||||
|
||||
func (l *FileLogger) Infof(format string, args ...interface{}) {
|
||||
l.logger.Printf("[INFO] "+format, args...)
|
||||
}
|
||||
|
||||
func (l *FileLogger) Warnf(format string, args ...interface{}) {
|
||||
l.logger.Printf("[WARN] "+format, args...)
|
||||
}
|
||||
|
||||
func (l *FileLogger) Errorf(format string, args ...interface{}) {
|
||||
l.logger.Printf("[ERROR] "+format, args...)
|
||||
}
|
||||
|
||||
func (l *FileLogger) Close() error {
|
||||
return l.file.Close()
|
||||
}
|
||||
|
||||
@@ -13,9 +13,14 @@ type App struct {
|
||||
currentModel tea.Model
|
||||
activityStorage *storage.ActivityStorage
|
||||
garminClient *garmin.Client
|
||||
logger garmin.Logger
|
||||
}
|
||||
|
||||
func NewApp(activityStorage *storage.ActivityStorage, garminClient *garmin.Client) *App {
|
||||
func NewApp(activityStorage *storage.ActivityStorage, garminClient *garmin.Client, logger garmin.Logger) *App {
|
||||
if logger == nil {
|
||||
logger = &garmin.NoopLogger{}
|
||||
}
|
||||
|
||||
// Initialize with the activity list screen as the default
|
||||
activityList := screens.NewActivityList(activityStorage, garminClient)
|
||||
|
||||
@@ -23,6 +28,7 @@ func NewApp(activityStorage *storage.ActivityStorage, garminClient *garmin.Clien
|
||||
currentModel: activityList,
|
||||
activityStorage: activityStorage,
|
||||
garminClient: garminClient,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,19 +38,24 @@ func (a *App) Init() tea.Cmd {
|
||||
|
||||
func (a *App) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case tea.WindowSizeMsg:
|
||||
// Forward window size to current model
|
||||
updatedModel, cmd := a.currentModel.Update(msg)
|
||||
a.currentModel = updatedModel
|
||||
return a, cmd
|
||||
case tea.KeyMsg:
|
||||
switch msg.String() {
|
||||
case "ctrl+c", "q":
|
||||
return a, tea.Quit
|
||||
}
|
||||
case screens.ActivitySelectedMsg:
|
||||
fmt.Printf("DEBUG: App.Update() - Received ActivitySelectedMsg for: %s\n", msg.Activity.Name)
|
||||
a.logger.Debugf("App.Update() - Received ActivitySelectedMsg for: %s", msg.Activity.Name)
|
||||
// For now, use empty analysis - we'll implement analysis caching later
|
||||
detail := screens.NewActivityDetail(msg.Activity, "")
|
||||
detail := screens.NewActivityDetail(msg.Activity, "", a.logger)
|
||||
a.currentModel = detail
|
||||
return a, detail.Init()
|
||||
case screens.BackToListMsg:
|
||||
fmt.Println("DEBUG: App.Update() - Received BackToListMsg")
|
||||
a.logger.Debugf("App.Update() - Received BackToListMsg")
|
||||
// Re-initialize the activity list when navigating back
|
||||
activityList := screens.NewActivityList(a.activityStorage, a.garminClient)
|
||||
a.currentModel = activityList
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
|
||||
"github.com/sstent/fitness-tui/internal/garmin"
|
||||
"github.com/sstent/fitness-tui/internal/tui/components"
|
||||
"github.com/sstent/fitness-tui/internal/tui/models"
|
||||
)
|
||||
@@ -23,6 +24,7 @@ type ActivityDetail struct {
|
||||
styles *Styles
|
||||
hrChart *components.Chart
|
||||
elevationChart *components.Chart
|
||||
logger garmin.Logger
|
||||
}
|
||||
|
||||
type Styles struct {
|
||||
@@ -34,7 +36,11 @@ type Styles struct {
|
||||
Viewport lipgloss.Style
|
||||
}
|
||||
|
||||
func NewActivityDetail(activity *models.Activity, analysis string) *ActivityDetail {
|
||||
func NewActivityDetail(activity *models.Activity, analysis string, logger garmin.Logger) *ActivityDetail {
|
||||
if logger == nil {
|
||||
logger = &garmin.NoopLogger{}
|
||||
}
|
||||
|
||||
styles := &Styles{
|
||||
Title: lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("5")),
|
||||
Subtitle: lipgloss.NewStyle().Foreground(lipgloss.Color("8")).MarginTop(1),
|
||||
@@ -50,6 +56,7 @@ func NewActivityDetail(activity *models.Activity, analysis string) *ActivityDeta
|
||||
analysis: analysis,
|
||||
viewport: vp,
|
||||
styles: styles,
|
||||
logger: logger,
|
||||
hrChart: components.NewChart(activity.Metrics.HeartRateData, 40, 4, "Heart Rate (bpm)"),
|
||||
elevationChart: components.NewChart(activity.Metrics.ElevationData, 40, 4, "Elevation (m)"),
|
||||
}
|
||||
@@ -76,7 +83,7 @@ func (m *ActivityDetail) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
case tea.KeyMsg:
|
||||
switch msg.String() {
|
||||
case "esc":
|
||||
return m, tea.Quit
|
||||
return m, func() tea.Msg { return BackToListMsg{} }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,13 +108,13 @@ func (m *ActivityDetail) setContent() {
|
||||
if m.activity == nil {
|
||||
content.WriteString("Activity data is nil!")
|
||||
m.viewport.SetContent(m.styles.Viewport.Render(content.String()))
|
||||
fmt.Println("DEBUG: ActivityDetail.setContent() - activity is nil")
|
||||
m.logger.Debugf("ActivityDetail.setContent() - activity is nil")
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("DEBUG: ActivityDetail.setContent() - Rendering activity: %s\n", m.activity.Name)
|
||||
fmt.Printf("DEBUG: ActivityDetail.setContent() - Duration: %v, Distance: %.2f\n", m.activity.Duration, m.activity.Distance)
|
||||
fmt.Printf("DEBUG: ActivityDetail.setContent() - Metrics: AvgHR=%d, MaxHR=%d, AvgSpeed=%.2f\n", m.activity.Metrics.AvgHeartRate, m.activity.Metrics.MaxHeartRate, m.activity.Metrics.AvgSpeed)
|
||||
m.logger.Debugf("ActivityDetail.setContent() - Rendering activity: %s", m.activity.Name)
|
||||
m.logger.Debugf("ActivityDetail.setContent() - Duration: %v, Distance: %.2f", m.activity.Duration, m.activity.Distance)
|
||||
m.logger.Debugf("ActivityDetail.setContent() - Metrics: AvgHR=%d, MaxHR=%d, AvgSpeed=%.2f", m.activity.Metrics.AvgHeartRate, m.activity.Metrics.MaxHeartRate, m.activity.Metrics.AvgSpeed)
|
||||
|
||||
// Debug info at top
|
||||
content.WriteString(fmt.Sprintf("DEBUG: Viewport W=%d H=%d, Activity: %s\n", m.width, m.height, m.activity.Name))
|
||||
|
||||
Reference in New Issue
Block a user