diff --git a/Dockerfile b/Dockerfile index 178475a..9ef2edc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -24,8 +24,8 @@ RUN CGO_ENABLED=1 GOOS=linux go build -o garminsync . # Runtime stage FROM alpine:3.18 -# Install runtime dependencies -RUN apk add --no-cache ca-certificates tzdata +# Install runtime dependencies (wget needed for healthcheck) +RUN apk add --no-cache ca-certificates tzdata wget sqlite # Create app directory WORKDIR /app @@ -33,14 +33,13 @@ WORKDIR /app # Copy binary from builder COPY --from=builder /app/garminsync /app/garminsync -# Copy templates -COPY internal/web/templates ./internal/web/templates +# Copy web directory (frontend assets) +COPY web ./web # Set timezone and environment ENV TZ=UTC \ DATA_DIR=/data \ - DB_PATH=/data/garmin.db \ - TEMPLATE_DIR=/app/internal/web/templates + DB_PATH=/data/garmin.db # Create data volume and set permissions RUN mkdir /data && chown nobody:nobody /data diff --git a/docker-compose.yml b/docker-compose.yml index 428c3c3..9a06e13 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,12 +6,8 @@ services: container_name: garminsync ports: - "8888:8888" - environment: - - GARMIN_EMAIL=${GARMIN_EMAIL} - - GARMIN_PASSWORD=${GARMIN_PASSWORD} - - DATA_DIR=/data - - DB_PATH=/data/garmin.db - - TEMPLATE_DIR=/app/internal/web/templates + env_file: + - .env # Use the root .env file volumes: - ./data:/data - ./internal/web/templates:/app/internal/web/templates diff --git a/internal/web/routes.go b/internal/web/routes.go index a610ed9..d206849 100644 --- a/internal/web/routes.go +++ b/internal/web/routes.go @@ -2,6 +2,7 @@ package web import ( "context" + "log" "net/http" "strconv" @@ -15,7 +16,6 @@ type WebHandler struct { db *database.SQLiteDB syncer *sync.SyncService garmin *garmin.Client - templates map[string]interface{} // Placeholder for template handling } func NewWebHandler(db *database.SQLiteDB, syncer *sync.SyncService, garmin *garmin.Client) *WebHandler { @@ -23,30 +23,22 @@ func NewWebHandler(db *database.SQLiteDB, syncer *sync.SyncService, garmin *garm db: db, syncer: syncer, garmin: garmin, - templates: make(map[string]interface{}), } } -func (h *WebHandler) LoadTemplates(templateDir string) error { - // For now, just return nil - templates will be handled later - return nil -} - -func (h *WebHandler) RegisterRoutes(router *gin.Engine) { - router.GET("/", h.Index) +func (h *WebHandler) RegisterRoutes(router *gin.RouterGroup) { + router.GET("/stats", h.GetStats) router.GET("/activities", h.ActivityList) router.GET("/activities/:id", h.ActivityDetail) router.POST("/sync", h.Sync) } -func (h *WebHandler) Index(c *gin.Context) { +func (h *WebHandler) GetStats(c *gin.Context) { stats, err := h.db.GetStats() if err != nil { - c.AbortWithStatus(http.StatusInternalServerError) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get stats"}) return } - - // Placeholder for template rendering c.JSON(http.StatusOK, stats) } @@ -60,7 +52,7 @@ func (h *WebHandler) ActivityList(c *gin.Context) { activities, err := h.db.GetActivities(limit, offset) if err != nil { - c.AbortWithStatus(http.StatusInternalServerError) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get activities"}) return } @@ -70,13 +62,13 @@ func (h *WebHandler) ActivityList(c *gin.Context) { func (h *WebHandler) ActivityDetail(c *gin.Context) { id, err := strconv.Atoi(c.Param("id")) if err != nil { - c.AbortWithStatus(http.StatusBadRequest) + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid activity ID"}) return } activity, err := h.db.GetActivity(id) if err != nil { - c.AbortWithStatus(http.StatusNotFound) + c.JSON(http.StatusNotFound, gin.H{"error": "Activity not found"}) return } @@ -84,11 +76,12 @@ func (h *WebHandler) ActivityDetail(c *gin.Context) { } func (h *WebHandler) Sync(c *gin.Context) { - err := h.syncer.Sync(context.Background()) - if err != nil { - c.AbortWithStatus(http.StatusInternalServerError) - return - } + go func() { + err := h.syncer.Sync(context.Background()) + if err != nil { + log.Printf("Sync error: %v", err) + } + }() - c.Status(http.StatusOK) + c.JSON(http.StatusOK, gin.H{"status": "sync_started", "message": "Sync started in background"}) } diff --git a/main.go b/main.go index d999b60..717f833 100644 --- a/main.go +++ b/main.go @@ -84,14 +84,7 @@ func (app *App) init() error { // Setup HTTP server webHandler := web.NewWebHandler(app.db, app.syncService, app.garmin) - templateDir := os.Getenv("TEMPLATE_DIR") - if templateDir == "" { - templateDir = "./internal/web/templates" - } - if err := webHandler.LoadTemplates(templateDir); err != nil { - return fmt.Errorf("failed to load templates: %w", err) - } - + // We've removed template loading since we're using static frontend app.server = &http.Server{ Addr: ":8888", Handler: app.setupRoutes(webHandler), @@ -183,13 +176,39 @@ func initDatabase() (*database.SQLiteDB, error) { func (app *App) setupRoutes(webHandler *web.WebHandler) http.Handler { router := gin.Default() + // Add middleware + router.Use(gin.Logger()) // Log all requests + router.Use(gin.Recovery()) // Recover from any panics + + // Enable CORS for development + router.Use(func(c *gin.Context) { + c.Header("Access-Control-Allow-Origin", "*") + c.Header("Access-Control-Allow-Methods", "GET, POST, OPTIONS") + c.Header("Access-Control-Allow-Headers", "Content-Type") + if c.Request.Method == "OPTIONS" { + c.AbortWithStatus(204) + return + } + c.Next() + }) + + // Serve static files + router.Static("/static", "./web/static") + router.LoadHTMLFiles("web/index.html") + + // API routes + api := router.Group("/api") + webHandler.RegisterRoutes(api) + + // Serve main page + router.GET("/", func(c *gin.Context) { + c.HTML(200, "index.html", nil) + }) + // Health check router.GET("/health", func(c *gin.Context) { c.String(http.StatusOK, "OK") }) - // Register web routes - webHandler.RegisterRoutes(router) - return router }