working build - with ui

This commit is contained in:
2025-08-25 18:03:20 -07:00
parent b370173873
commit 9377db0164
4 changed files with 52 additions and 45 deletions

View File

@@ -24,8 +24,8 @@ RUN CGO_ENABLED=1 GOOS=linux go build -o garminsync .
# Runtime stage # Runtime stage
FROM alpine:3.18 FROM alpine:3.18
# Install runtime dependencies # Install runtime dependencies (wget needed for healthcheck)
RUN apk add --no-cache ca-certificates tzdata RUN apk add --no-cache ca-certificates tzdata wget sqlite
# Create app directory # Create app directory
WORKDIR /app WORKDIR /app
@@ -33,14 +33,13 @@ WORKDIR /app
# Copy binary from builder # Copy binary from builder
COPY --from=builder /app/garminsync /app/garminsync COPY --from=builder /app/garminsync /app/garminsync
# Copy templates # Copy web directory (frontend assets)
COPY internal/web/templates ./internal/web/templates COPY web ./web
# Set timezone and environment # Set timezone and environment
ENV TZ=UTC \ ENV TZ=UTC \
DATA_DIR=/data \ DATA_DIR=/data \
DB_PATH=/data/garmin.db \ DB_PATH=/data/garmin.db
TEMPLATE_DIR=/app/internal/web/templates
# Create data volume and set permissions # Create data volume and set permissions
RUN mkdir /data && chown nobody:nobody /data RUN mkdir /data && chown nobody:nobody /data

View File

@@ -6,12 +6,8 @@ services:
container_name: garminsync container_name: garminsync
ports: ports:
- "8888:8888" - "8888:8888"
environment: env_file:
- GARMIN_EMAIL=${GARMIN_EMAIL} - .env # Use the root .env file
- GARMIN_PASSWORD=${GARMIN_PASSWORD}
- DATA_DIR=/data
- DB_PATH=/data/garmin.db
- TEMPLATE_DIR=/app/internal/web/templates
volumes: volumes:
- ./data:/data - ./data:/data
- ./internal/web/templates:/app/internal/web/templates - ./internal/web/templates:/app/internal/web/templates

View File

@@ -2,6 +2,7 @@ package web
import ( import (
"context" "context"
"log"
"net/http" "net/http"
"strconv" "strconv"
@@ -15,7 +16,6 @@ type WebHandler struct {
db *database.SQLiteDB db *database.SQLiteDB
syncer *sync.SyncService syncer *sync.SyncService
garmin *garmin.Client garmin *garmin.Client
templates map[string]interface{} // Placeholder for template handling
} }
func NewWebHandler(db *database.SQLiteDB, syncer *sync.SyncService, garmin *garmin.Client) *WebHandler { 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, db: db,
syncer: syncer, syncer: syncer,
garmin: garmin, garmin: garmin,
templates: make(map[string]interface{}),
} }
} }
func (h *WebHandler) LoadTemplates(templateDir string) error { func (h *WebHandler) RegisterRoutes(router *gin.RouterGroup) {
// For now, just return nil - templates will be handled later router.GET("/stats", h.GetStats)
return nil
}
func (h *WebHandler) RegisterRoutes(router *gin.Engine) {
router.GET("/", h.Index)
router.GET("/activities", h.ActivityList) router.GET("/activities", h.ActivityList)
router.GET("/activities/:id", h.ActivityDetail) router.GET("/activities/:id", h.ActivityDetail)
router.POST("/sync", h.Sync) router.POST("/sync", h.Sync)
} }
func (h *WebHandler) Index(c *gin.Context) { func (h *WebHandler) GetStats(c *gin.Context) {
stats, err := h.db.GetStats() stats, err := h.db.GetStats()
if err != nil { if err != nil {
c.AbortWithStatus(http.StatusInternalServerError) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get stats"})
return return
} }
// Placeholder for template rendering
c.JSON(http.StatusOK, stats) c.JSON(http.StatusOK, stats)
} }
@@ -60,7 +52,7 @@ func (h *WebHandler) ActivityList(c *gin.Context) {
activities, err := h.db.GetActivities(limit, offset) activities, err := h.db.GetActivities(limit, offset)
if err != nil { if err != nil {
c.AbortWithStatus(http.StatusInternalServerError) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get activities"})
return return
} }
@@ -70,13 +62,13 @@ func (h *WebHandler) ActivityList(c *gin.Context) {
func (h *WebHandler) ActivityDetail(c *gin.Context) { func (h *WebHandler) ActivityDetail(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id")) id, err := strconv.Atoi(c.Param("id"))
if err != nil { if err != nil {
c.AbortWithStatus(http.StatusBadRequest) c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid activity ID"})
return return
} }
activity, err := h.db.GetActivity(id) activity, err := h.db.GetActivity(id)
if err != nil { if err != nil {
c.AbortWithStatus(http.StatusNotFound) c.JSON(http.StatusNotFound, gin.H{"error": "Activity not found"})
return return
} }
@@ -84,11 +76,12 @@ func (h *WebHandler) ActivityDetail(c *gin.Context) {
} }
func (h *WebHandler) Sync(c *gin.Context) { func (h *WebHandler) Sync(c *gin.Context) {
err := h.syncer.Sync(context.Background()) go func() {
if err != nil { err := h.syncer.Sync(context.Background())
c.AbortWithStatus(http.StatusInternalServerError) if err != nil {
return log.Printf("Sync error: %v", err)
} }
}()
c.Status(http.StatusOK) c.JSON(http.StatusOK, gin.H{"status": "sync_started", "message": "Sync started in background"})
} }

41
main.go
View File

@@ -84,14 +84,7 @@ func (app *App) init() error {
// Setup HTTP server // Setup HTTP server
webHandler := web.NewWebHandler(app.db, app.syncService, app.garmin) webHandler := web.NewWebHandler(app.db, app.syncService, app.garmin)
templateDir := os.Getenv("TEMPLATE_DIR") // We've removed template loading since we're using static frontend
if templateDir == "" {
templateDir = "./internal/web/templates"
}
if err := webHandler.LoadTemplates(templateDir); err != nil {
return fmt.Errorf("failed to load templates: %w", err)
}
app.server = &http.Server{ app.server = &http.Server{
Addr: ":8888", Addr: ":8888",
Handler: app.setupRoutes(webHandler), Handler: app.setupRoutes(webHandler),
@@ -183,13 +176,39 @@ func initDatabase() (*database.SQLiteDB, error) {
func (app *App) setupRoutes(webHandler *web.WebHandler) http.Handler { func (app *App) setupRoutes(webHandler *web.WebHandler) http.Handler {
router := gin.Default() 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 // Health check
router.GET("/health", func(c *gin.Context) { router.GET("/health", func(c *gin.Context) {
c.String(http.StatusOK, "OK") c.String(http.StatusOK, "OK")
}) })
// Register web routes
webHandler.RegisterRoutes(router)
return router return router
} }