mirror of
https://github.com/R0m1k3/FlowReader.git
synced 2026-10-24 15:48:06 +02:00
feat(story-1.1): implement walking skeleton with Go Chi and Docker Compose
This commit is contained in:
1 parent
d7ad2536d6
commit
99f1300e13
11 files changed
+411
No files matched your search
+34
@@ -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"]
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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:
|
||||||
@@ -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
|
||||||
|
)
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
// Package domain contains business logic entities and interfaces.
|
||||||
|
// This package defines the core types used throughout the application.
|
||||||
|
package domain
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
// Package repository contains database access implementations.
|
||||||
|
// Repositories use raw SQL with pgx - No ORM.
|
||||||
|
package repository
|
||||||
@@ -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
|
||||||
|
```
|
||||||
Reference in new issue
Block a user