This commit is contained in:
2025-09-13 11:46:02 -07:00
parent 8250a9565c
commit e923b10cf7
6 changed files with 117 additions and 14 deletions

View File

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

View File

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

View File

@@ -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()
}

View File

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

View File

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