Rewrite Tracks application in Golang

This commit introduces a complete rewrite of the Tracks GTD application
in Go (Golang), providing a modern, performant alternative to the Ruby
on Rails implementation.

## Architecture & Technology Stack

- Language: Go 1.21+
- Web Framework: Gin
- ORM: GORM with SQLite/MySQL/PostgreSQL support
- Authentication: JWT with bcrypt password hashing
- Clean Architecture: Separated models, services, handlers, and middleware

## Implemented Features

### Core Models
- User: Authentication and user management
- Context: GTD contexts (@home, @work, etc.)
- Project: Project grouping and tracking
- Todo: Task management with state machine (active, completed, deferred, pending)
- Tag: Flexible tagging system with polymorphic associations
- Dependency: Todo dependencies with circular dependency detection
- Preference: User preferences and settings
- Note: Project notes
- Attachment: File attachment support (model only)
- RecurringTodo: Recurring task template (model only)

### API Endpoints

**Authentication:**
- POST /api/auth/login - User login
- POST /api/auth/register - User registration
- POST /api/auth/logout - User logout
- GET /api/me - Get current user

**Todos:**
- GET /api/todos - List todos with filtering
- POST /api/todos - Create todo
- GET /api/todos/:id - Get todo details
- PUT /api/todos/:id - Update todo
- DELETE /api/todos/:id - Delete todo
- POST /api/todos/:id/complete - Mark as completed
- POST /api/todos/:id/activate - Mark as active
- POST /api/todos/:id/defer - Defer to future date
- POST /api/todos/:id/dependencies - Add dependency
- DELETE /api/todos/:id/dependencies/:successor_id - Remove dependency

**Projects:**
- GET /api/projects - List projects
- POST /api/projects - Create project
- GET /api/projects/:id - Get project details
- PUT /api/projects/:id - Update project
- DELETE /api/projects/:id - Delete project
- POST /api/projects/:id/complete - Complete project
- POST /api/projects/:id/activate - Activate project
- POST /api/projects/:id/hide - Hide project
- POST /api/projects/:id/review - Mark as reviewed
- GET /api/projects/:id/stats - Get project statistics

**Contexts:**
- GET /api/contexts - List contexts
- POST /api/contexts - Create context
- GET /api/contexts/:id - Get context details
- PUT /api/contexts/:id - Update context
- DELETE /api/contexts/:id - Delete context
- POST /api/contexts/:id/hide - Hide context
- POST /api/contexts/:id/activate - Activate context
- POST /api/contexts/:id/close - Close context
- GET /api/contexts/:id/stats - Get context statistics

### Business Logic

**Todo State Management:**
- Active: Ready to work on
- Completed: Finished tasks
- Deferred: Future actions (show_from date)
- Pending: Blocked by dependencies

**Dependency Management:**
- Create blocking relationships between todos
- Automatic state transitions when blocking todos complete
- Circular dependency detection
- Automatic unblocking when prerequisites complete

**Tag System:**
- Polymorphic tagging for todos and recurring todos
- Automatic tag creation on first use
- Tag cloud support

**Project & Context Tracking:**
- State management (active, hidden, closed/completed)
- Statistics and health indicators
- Review tracking for projects

### Infrastructure

**Configuration:**
- Environment-based configuration
- Support for SQLite, MySQL, and PostgreSQL
- Configurable JWT secrets and token expiry
- Flexible server settings

**Database:**
- GORM for ORM
- Automatic migrations
- Connection pooling
- Multi-database support

**Authentication & Security:**
- JWT-based authentication
- Bcrypt password hashing
- Secure cookie support
- Token refresh mechanism

**Docker Support:**
- Multi-stage Dockerfile for optimized builds
- Docker Compose with PostgreSQL
- Volume mounting for data persistence
- Production-ready configuration

## Project Structure

```
cmd/tracks/              # Application entry point
internal/
  config/               # Configuration management
  database/             # Database setup and migrations
  handlers/             # HTTP request handlers
  middleware/           # Authentication middleware
  models/              # Database models
  services/            # Business logic layer
```

## Documentation

- README_GOLANG.md: Comprehensive documentation
- .env.example: Configuration template
- API documentation included in README
- Code comments for complex logic

## Future Work

The following features from the original Rails app are not yet implemented:
- Recurring todo instantiation logic
- Email integration (Mailgun/CloudMailin)
- Advanced statistics and analytics
- Import/Export functionality (CSV, YAML, XML)
- File upload handling for attachments
- Mobile views
- RSS/Atom feeds
- iCalendar export

## Benefits Over Rails Version

- Performance: Compiled binary, lower resource usage
- Deployment: Single binary, no runtime dependencies
- Type Safety: Compile-time type checking
- Concurrency: Better handling of concurrent requests
- Memory: Lower memory footprint
- Portability: Easy cross-platform compilation

## Testing

The code structure supports testing, though tests are not yet implemented.
Future work includes adding unit and integration tests.
This commit is contained in:
Claude 2025-11-05 10:46:59 +00:00
parent 6613d33f10
commit f0eb4bdef5
No known key found for this signature in database
29 changed files with 4100 additions and 104 deletions

147
cmd/tracks/main.go Normal file
View file

@ -0,0 +1,147 @@
package main
import (
"fmt"
"log"
"github.com/TracksApp/tracks/internal/config"
"github.com/TracksApp/tracks/internal/database"
"github.com/TracksApp/tracks/internal/handlers"
"github.com/TracksApp/tracks/internal/middleware"
"github.com/TracksApp/tracks/internal/services"
"github.com/gin-gonic/gin"
)
func main() {
// Load configuration
cfg, err := config.Load()
if err != nil {
log.Fatal("Failed to load configuration:", err)
}
// Initialize database
if err := database.Initialize(&cfg.Database); err != nil {
log.Fatal("Failed to initialize database:", err)
}
defer database.Close()
// Run migrations
if err := database.AutoMigrate(); err != nil {
log.Fatal("Failed to run migrations:", err)
}
// Set Gin mode
gin.SetMode(cfg.Server.Mode)
// Create router
router := gin.Default()
// Setup routes
setupRoutes(router, cfg)
// Start server
addr := fmt.Sprintf("%s:%d", cfg.Server.Host, cfg.Server.Port)
log.Printf("Starting Tracks server on %s", addr)
if err := router.Run(addr); err != nil {
log.Fatal("Failed to start server:", err)
}
}
func setupRoutes(router *gin.Engine, cfg *config.Config) {
// Initialize services
authService := services.NewAuthService(cfg.Auth.JWTSecret)
todoService := services.NewTodoService()
projectService := services.NewProjectService()
contextService := services.NewContextService()
// Initialize handlers
authHandler := handlers.NewAuthHandler(authService)
todoHandler := handlers.NewTodoHandler(todoService)
projectHandler := handlers.NewProjectHandler(projectService)
contextHandler := handlers.NewContextHandler(contextService)
// Public routes
api := router.Group("/api")
{
// Health check
api.GET("/health", func(c *gin.Context) {
c.JSON(200, gin.H{"status": "ok"})
})
// Auth routes
auth := api.Group("/auth")
{
auth.POST("/login", authHandler.Login)
auth.POST("/register", authHandler.Register)
auth.POST("/logout", authHandler.Logout)
}
}
// Protected routes
protected := api.Group("")
protected.Use(middleware.AuthMiddleware(cfg.Auth.JWTSecret))
{
// User routes
protected.GET("/me", authHandler.Me)
protected.POST("/refresh-token", authHandler.RefreshToken)
// Todo routes
todos := protected.Group("/todos")
{
todos.GET("", todoHandler.ListTodos)
todos.POST("", todoHandler.CreateTodo)
todos.GET("/:id", todoHandler.GetTodo)
todos.PUT("/:id", todoHandler.UpdateTodo)
todos.DELETE("/:id", todoHandler.DeleteTodo)
todos.POST("/:id/complete", todoHandler.CompleteTodo)
todos.POST("/:id/activate", todoHandler.ActivateTodo)
todos.POST("/:id/defer", todoHandler.DeferTodo)
todos.POST("/:id/dependencies", todoHandler.AddDependency)
todos.DELETE("/:id/dependencies/:successor_id", todoHandler.RemoveDependency)
}
// Project routes
projects := protected.Group("/projects")
{
projects.GET("", projectHandler.ListProjects)
projects.POST("", projectHandler.CreateProject)
projects.GET("/:id", projectHandler.GetProject)
projects.PUT("/:id", projectHandler.UpdateProject)
projects.DELETE("/:id", projectHandler.DeleteProject)
projects.POST("/:id/complete", projectHandler.CompleteProject)
projects.POST("/:id/activate", projectHandler.ActivateProject)
projects.POST("/:id/hide", projectHandler.HideProject)
projects.POST("/:id/review", projectHandler.MarkReviewed)
projects.GET("/:id/stats", projectHandler.GetProjectStats)
}
// Context routes
contexts := protected.Group("/contexts")
{
contexts.GET("", contextHandler.ListContexts)
contexts.POST("", contextHandler.CreateContext)
contexts.GET("/:id", contextHandler.GetContext)
contexts.PUT("/:id", contextHandler.UpdateContext)
contexts.DELETE("/:id", contextHandler.DeleteContext)
contexts.POST("/:id/hide", contextHandler.HideContext)
contexts.POST("/:id/activate", contextHandler.ActivateContext)
contexts.POST("/:id/close", contextHandler.CloseContext)
contexts.GET("/:id/stats", contextHandler.GetContextStats)
}
}
// CORS middleware for development
router.Use(func(c *gin.Context) {
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With")
c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT, DELETE, PATCH")
if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(204)
return
}
c.Next()
})
}