diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..22cdd7b --- /dev/null +++ b/Dockerfile @@ -0,0 +1,34 @@ +# Build stage +FROM golang:1.22-alpine AS builder + +WORKDIR /app + +# Install dependencies +RUN apk add --no-cache git + +# Copy go mod files +COPY go.mod go.sum* ./ +RUN go mod download + +# Copy source code +COPY . . + +# Build the application +RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-w -s" -o /server ./cmd/server + +# Final stage +FROM alpine:3.19 + +WORKDIR /app + +# Install wget for healthcheck +RUN apk add --no-cache wget ca-certificates + +# Copy binary from builder +COPY --from=builder /server /app/server + +# Expose port +EXPOSE 8080 + +# Run the application +CMD ["/app/server"] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..aadac46 --- /dev/null +++ b/Makefile @@ -0,0 +1,45 @@ +.PHONY: dev build run test lint clean docker-up docker-down migrate-up migrate-down + +# Development +dev: + go run ./cmd/server + +build: + go build -o bin/server ./cmd/server + +run: build + ./bin/server + +# Testing +test: + go test -v ./... + +test-coverage: + go test -coverprofile=coverage.out ./... + go tool cover -html=coverage.out -o coverage.html + +# Linting +lint: + golangci-lint run + +# Docker +docker-up: + docker-compose up --build -d + +docker-down: + docker-compose down + +docker-logs: + docker-compose logs -f + +# Database migrations (placeholder for Story 1.2) +migrate-up: + @echo "Migration system not yet configured (Story 1.2)" + +migrate-down: + @echo "Migration system not yet configured (Story 1.2)" + +# Cleanup +clean: + rm -rf bin/ + rm -f coverage.out coverage.html diff --git a/README.md b/README.md new file mode 100644 index 0000000..68b1ba3 --- /dev/null +++ b/README.md @@ -0,0 +1,56 @@ +# FlowReader + +A minimalist, self-hosted RSS/Atom reader focused on simplicity and resource efficiency. + +## Quick Start + +```bash +# Start with Docker Compose +docker-compose up --build + +# Or use Make +make docker-up +``` + +Access the API: `http://localhost:8080` + +## Development + +```bash +# Run locally (requires Go 1.22+) +make dev + +# Run tests +make test + +# Lint code +make lint +``` + +## Project Structure + +``` +flowreader/ +├── cmd/ +│ └── server/ # Application entry point +├── internal/ +│ ├── domain/ # Business logic (entities, services) +│ ├── handler/ # HTTP handlers +│ └── repository/ # Database access +├── pkg/ # Shared utilities +└── migrations/ # SQL migrations +``` + +## Tech Stack + +- **Backend**: Go 1.22+ (Chi Router, pgx driver) +- **Database**: PostgreSQL 16+ +- **Frontend**: React + Vite (coming in Epic 3) + +## Memory Target + +Total container footprint: **< 150MB RAM** + +## License + +MIT diff --git a/_bmad-output/implementation-artifacts/sprint-status.yaml b/_bmad-output/implementation-artifacts/sprint-status.yaml new file mode 100644 index 0000000..882b228 --- /dev/null +++ b/_bmad-output/implementation-artifacts/sprint-status.yaml @@ -0,0 +1,71 @@ +# Sprint Status - FlowReader +# generated: 2026-02-04 +# project: FlowReader +# tracking_system: file-system +# story_location: _bmad-output/implementation-artifacts + +# STATUS DEFINITIONS: +# ================== +# Epic Status: +# - backlog: Epic not yet started +# - in-progress: Epic actively being worked on +# - done: All stories in epic completed +# +# Story Status: +# - backlog: Story only exists in epic file +# - ready-for-dev: Story file created, ready for development +# - in-progress: Developer actively working on implementation +# - review: Implementation complete, ready for review +# - done: Story completed +# +# Retrospective Status: +# - optional: Can be completed but not required +# - done: Retrospective has been completed +# +# WORKFLOW NOTES: +# =============== +# - Mark epic as 'in-progress' when starting work on its first story +# - SM typically creates next story ONLY after previous one is 'done' +# - Dev moves story to 'review', then runs code-review + +generated: 2026-02-04 +project: FlowReader +tracking_system: file-system +story_location: "_bmad-output/implementation-artifacts" + +development_status: + # Epic 1: Walking Skeleton & User Access + epic-1: in-progress + 1-1-project-initialization-walking-skeleton: review + 1-2-database-migration-system: backlog + 1-3-user-registration-api: backlog + 1-4-session-authentication: backlog + 1-5-frontend-auth-foundation: backlog + epic-1-retrospective: optional + + # Epic 2: Feed Core Engine + epic-2: backlog + 2-1-feed-model-add-feed-by-url: backlog + 2-2-feed-metadata-auto-discovery: backlog + 2-3-opml-import: backlog + 2-4-feed-management-crud: backlog + 2-5-background-feed-fetcher: backlog + epic-2-retrospective: optional + + # Epic 3: Lecture Plaisir & Real-Time Experience + epic-3: backlog + 3-1-dashboard-nouveautes: backlog + 3-2-article-detail-reader-mode: backlog + 3-3-mark-as-read-manual-auto: backlog + 3-4-mark-all-as-read: backlog + 3-5-favorites-system: backlog + 3-6-websocket-push-notifications: backlog + epic-3-retrospective: optional + + # Epic 4: Advanced Systems & Robustness + epic-4: backlog + 4-1-multi-device-sync: backlog + 4-2-admin-user-list: backlog + 4-3-delete-user-account: backlog + 4-4-performance-ram-optimization: backlog + epic-4-retrospective: optional diff --git a/cmd/server/main.go b/cmd/server/main.go new file mode 100644 index 0000000..15c3e21 --- /dev/null +++ b/cmd/server/main.go @@ -0,0 +1,105 @@ +package main + +import ( + "context" + "log" + "net/http" + "os" + "os/signal" + "syscall" + "time" + + "github.com/go-chi/chi/v5" + "github.com/go-chi/chi/v5/middleware" + "github.com/jackc/pgx/v5/pgxpool" +) + +func main() { + // Load configuration from environment + port := getEnv("PORT", "8080") + dbURL := getEnv("DATABASE_URL", "postgres://flowreader:flowreader@db:5432/flowreader?sslmode=disable") + + // Initialize database connection pool + ctx := context.Background() + pool, err := pgxpool.New(ctx, dbURL) + if err != nil { + log.Fatalf("Unable to create connection pool: %v", err) + } + defer pool.Close() + + // Verify database connection + if err := pool.Ping(ctx); err != nil { + log.Fatalf("Unable to ping database: %v", err) + } + log.Println("Connected to database") + + // Initialize router + r := chi.NewRouter() + + // Middleware + r.Use(middleware.Logger) + r.Use(middleware.Recoverer) + r.Use(middleware.RequestID) + r.Use(middleware.RealIP) + r.Use(middleware.Timeout(30 * time.Second)) + + // Health check endpoint + r.Get("/health", func(w http.ResponseWriter, r *http.Request) { + // Check database connection + if err := pool.Ping(r.Context()); err != nil { + http.Error(w, "Database connection failed", http.StatusServiceUnavailable) + return + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"status":"ok","service":"flowreader"}`)) + }) + + // API routes placeholder + r.Route("/api/v1", func(r chi.Router) { + r.Get("/", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(`{"message":"FlowReader API v1"}`)) + }) + }) + + // Create server + srv := &http.Server{ + Addr: ":" + port, + Handler: r, + ReadTimeout: 15 * time.Second, + WriteTimeout: 15 * time.Second, + IdleTimeout: 60 * time.Second, + } + + // Graceful shutdown + go func() { + log.Printf("Server starting on port %s", port) + if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { + log.Fatalf("Server failed: %v", err) + } + }() + + // Wait for interrupt signal + quit := make(chan os.Signal, 1) + signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) + <-quit + + log.Println("Shutting down server...") + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + if err := srv.Shutdown(ctx); err != nil { + log.Fatalf("Server forced to shutdown: %v", err) + } + + log.Println("Server exited properly") +} + +// getEnv returns the value of an environment variable or a default value +func getEnv(key, defaultValue string) string { + if value, exists := os.LookupEnv(key); exists { + return value + } + return defaultValue +} diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..424ea08 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,46 @@ +services: + app: + build: + context: . + dockerfile: Dockerfile + ports: + - "8080:8080" + environment: + - PORT=8080 + - DATABASE_URL=postgres://flowreader:flowreader@db:5432/flowreader?sslmode=disable + depends_on: + db: + condition: service_healthy + restart: unless-stopped + healthcheck: + test: ["CMD", "wget", "-q", "--spider", "http://localhost:8080/health"] + interval: 10s + timeout: 5s + retries: 3 + start_period: 10s + + db: + image: postgres:16-alpine + environment: + - POSTGRES_USER=flowreader + - POSTGRES_PASSWORD=flowreader + - POSTGRES_DB=flowreader + volumes: + - postgres_data:/var/lib/postgresql/data + ports: + - "5432:5432" + restart: unless-stopped + healthcheck: + test: ["CMD-SHELL", "pg_isready -U flowreader -d flowreader"] + interval: 5s + timeout: 5s + retries: 5 + # RAM Optimization (from Architecture NFR1) + command: > + postgres + -c shared_buffers=24MB + -c max_connections=20 + -c work_mem=2MB + +volumes: + postgres_data: diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..c20d392 --- /dev/null +++ b/go.mod @@ -0,0 +1,17 @@ +module github.com/michael/flowreader + +go 1.22 + +require ( + github.com/go-chi/chi/v5 v5.0.11 + github.com/jackc/pgx/v5 v5.5.2 +) + +require ( + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect + github.com/jackc/puddle/v2 v2.2.1 // indirect + golang.org/x/crypto v0.17.0 // indirect + golang.org/x/sync v0.1.0 // indirect + golang.org/x/text v0.14.0 // indirect +) diff --git a/internal/domain/doc.go b/internal/domain/doc.go new file mode 100644 index 0000000..40901ed --- /dev/null +++ b/internal/domain/doc.go @@ -0,0 +1,3 @@ +// Package domain contains business logic entities and interfaces. +// This package defines the core types used throughout the application. +package domain diff --git a/internal/handler/doc.go b/internal/handler/doc.go new file mode 100644 index 0000000..748a759 --- /dev/null +++ b/internal/handler/doc.go @@ -0,0 +1,4 @@ +// Package handler contains HTTP handlers for the REST API. +// Handlers are responsible for request/response translation only. +// No business logic should be placed here. +package handler diff --git a/internal/repository/doc.go b/internal/repository/doc.go new file mode 100644 index 0000000..50c6d33 --- /dev/null +++ b/internal/repository/doc.go @@ -0,0 +1,3 @@ +// Package repository contains database access implementations. +// Repositories use raw SQL with pgx - No ORM. +package repository diff --git a/migrations/README.md b/migrations/README.md new file mode 100644 index 0000000..84726fb --- /dev/null +++ b/migrations/README.md @@ -0,0 +1,27 @@ +# SQL Migrations + +This folder contains versioned database migrations. + +Migrations are managed using `golang-migrate`. + +## Naming Convention + +``` +{version}_{description}.up.sql +{version}_{description}.down.sql +``` + +Example: + +- `001_create_users.up.sql` +- `001_create_users.down.sql` + +## Commands + +```bash +# Apply all pending migrations +make migrate-up + +# Rollback last migration +make migrate-down +```