mirror of
https://github.com/sstent/aicyclingcoach-go.git
synced 2026-02-13 19:06:59 +00:00
sync
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/sstent/fitness-tui/internal/config"
|
"github.com/sstent/fitness-tui/internal/config"
|
||||||
@@ -82,10 +83,19 @@ func runTUI() {
|
|||||||
os.Exit(1)
|
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)
|
activityStorage := storage.NewActivityStorage(cfg.StoragePath)
|
||||||
garminClient := garmin.NewClient(cfg.Garmin.Username, cfg.Garmin.Password, 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 {
|
if err := app.Run(); err != nil {
|
||||||
fmt.Printf("Application error: %v\n", err)
|
fmt.Printf("Application error: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
|
|||||||
Binary file not shown.
@@ -98,7 +98,8 @@ func (c *Client) GetActivities(ctx context.Context, limit int, logger Logger) ([
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
activities = append(activities, &models.Activity{
|
// Convert garth activity to internal model
|
||||||
|
activity := &models.Activity{
|
||||||
ID: fmt.Sprintf("%d", ga.ActivityID),
|
ID: fmt.Sprintf("%d", ga.ActivityID),
|
||||||
Name: ga.ActivityName,
|
Name: ga.ActivityName,
|
||||||
Type: ga.ActivityType.TypeKey,
|
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,
|
Duration: time.Duration(ga.Duration) * time.Second,
|
||||||
Elevation: ga.ElevationGain,
|
Elevation: ga.ElevationGain,
|
||||||
Calories: int(ga.Calories),
|
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))
|
logger.Infof("Successfully fetched %d activities", len(activities))
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
package garmin
|
package garmin
|
||||||
|
|
||||||
import "fmt"
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
// Logger defines the interface for logging in Garmin operations
|
// Logger defines the interface for logging in Garmin operations
|
||||||
type Logger interface {
|
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) Infof(format string, args ...interface{}) {}
|
||||||
func (l *NoopLogger) Warnf(format string, args ...interface{}) {}
|
func (l *NoopLogger) Warnf(format string, args ...interface{}) {}
|
||||||
func (l *NoopLogger) Errorf(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
|
currentModel tea.Model
|
||||||
activityStorage *storage.ActivityStorage
|
activityStorage *storage.ActivityStorage
|
||||||
garminClient *garmin.Client
|
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
|
// Initialize with the activity list screen as the default
|
||||||
activityList := screens.NewActivityList(activityStorage, garminClient)
|
activityList := screens.NewActivityList(activityStorage, garminClient)
|
||||||
|
|
||||||
@@ -23,6 +28,7 @@ func NewApp(activityStorage *storage.ActivityStorage, garminClient *garmin.Clien
|
|||||||
currentModel: activityList,
|
currentModel: activityList,
|
||||||
activityStorage: activityStorage,
|
activityStorage: activityStorage,
|
||||||
garminClient: garminClient,
|
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) {
|
func (a *App) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
switch msg := msg.(type) {
|
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:
|
case tea.KeyMsg:
|
||||||
switch msg.String() {
|
switch msg.String() {
|
||||||
case "ctrl+c", "q":
|
case "ctrl+c", "q":
|
||||||
return a, tea.Quit
|
return a, tea.Quit
|
||||||
}
|
}
|
||||||
case screens.ActivitySelectedMsg:
|
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
|
// 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
|
a.currentModel = detail
|
||||||
return a, detail.Init()
|
return a, detail.Init()
|
||||||
case screens.BackToListMsg:
|
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
|
// Re-initialize the activity list when navigating back
|
||||||
activityList := screens.NewActivityList(a.activityStorage, a.garminClient)
|
activityList := screens.NewActivityList(a.activityStorage, a.garminClient)
|
||||||
a.currentModel = activityList
|
a.currentModel = activityList
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
"github.com/charmbracelet/lipgloss"
|
"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/components"
|
||||||
"github.com/sstent/fitness-tui/internal/tui/models"
|
"github.com/sstent/fitness-tui/internal/tui/models"
|
||||||
)
|
)
|
||||||
@@ -23,6 +24,7 @@ type ActivityDetail struct {
|
|||||||
styles *Styles
|
styles *Styles
|
||||||
hrChart *components.Chart
|
hrChart *components.Chart
|
||||||
elevationChart *components.Chart
|
elevationChart *components.Chart
|
||||||
|
logger garmin.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
type Styles struct {
|
type Styles struct {
|
||||||
@@ -34,7 +36,11 @@ type Styles struct {
|
|||||||
Viewport lipgloss.Style
|
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{
|
styles := &Styles{
|
||||||
Title: lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("5")),
|
Title: lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("5")),
|
||||||
Subtitle: lipgloss.NewStyle().Foreground(lipgloss.Color("8")).MarginTop(1),
|
Subtitle: lipgloss.NewStyle().Foreground(lipgloss.Color("8")).MarginTop(1),
|
||||||
@@ -50,6 +56,7 @@ func NewActivityDetail(activity *models.Activity, analysis string) *ActivityDeta
|
|||||||
analysis: analysis,
|
analysis: analysis,
|
||||||
viewport: vp,
|
viewport: vp,
|
||||||
styles: styles,
|
styles: styles,
|
||||||
|
logger: logger,
|
||||||
hrChart: components.NewChart(activity.Metrics.HeartRateData, 40, 4, "Heart Rate (bpm)"),
|
hrChart: components.NewChart(activity.Metrics.HeartRateData, 40, 4, "Heart Rate (bpm)"),
|
||||||
elevationChart: components.NewChart(activity.Metrics.ElevationData, 40, 4, "Elevation (m)"),
|
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:
|
case tea.KeyMsg:
|
||||||
switch msg.String() {
|
switch msg.String() {
|
||||||
case "esc":
|
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 {
|
if m.activity == nil {
|
||||||
content.WriteString("Activity data is nil!")
|
content.WriteString("Activity data is nil!")
|
||||||
m.viewport.SetContent(m.styles.Viewport.Render(content.String()))
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("DEBUG: ActivityDetail.setContent() - Rendering activity: %s\n", m.activity.Name)
|
m.logger.Debugf("ActivityDetail.setContent() - Rendering activity: %s", m.activity.Name)
|
||||||
fmt.Printf("DEBUG: ActivityDetail.setContent() - Duration: %v, Distance: %.2f\n", m.activity.Duration, m.activity.Distance)
|
m.logger.Debugf("ActivityDetail.setContent() - Duration: %v, Distance: %.2f", 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() - Metrics: AvgHR=%d, MaxHR=%d, AvgSpeed=%.2f", m.activity.Metrics.AvgHeartRate, m.activity.Metrics.MaxHeartRate, m.activity.Metrics.AvgSpeed)
|
||||||
|
|
||||||
// Debug info at top
|
// Debug info at top
|
||||||
content.WriteString(fmt.Sprintf("DEBUG: Viewport W=%d H=%d, Activity: %s\n", m.width, m.height, m.activity.Name))
|
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