diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..24cbd50 --- /dev/null +++ b/.env.example @@ -0,0 +1 @@ +ROOT_DIR=./data diff --git a/.gitignore b/.gitignore index 4f2f97d..d5beae2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,14 +1,15 @@ # binary sprintpadawan +server +main -# sqlite db +# db app.db +app.db-wal # env .env # os .DS_Store -Thumbs.db -app.db-wal -server +.direnv/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..36a3ea1 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,21 @@ +# Build stage +FROM golang:alpine AS builder +RUN apk add --no-cache build-base +WORKDIR /app +COPY go.mod go.sum ./ +RUN go mod download +COPY . . +RUN CGO_ENABLED=1 GOOS=linux go build -o sprintpadawan main.go + +# Runtime stage +FROM alpine:latest +RUN apk --no-cache add ca-certificates tzdata +WORKDIR /app + +ENV ROOT_DIR=/data +RUN mkdir -p /data + +COPY --from=builder /app/sprintpadawan . + +EXPOSE 8080 +CMD ["./sprintpadawan"] diff --git a/README.md b/README.md index 6d4f7fe..0daf4c1 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,29 @@ -# sprintpadawan +# SprintPadawan +A lightweight real-time sprint planning tool. Built with Go, HTMX, and Turso (The FOSS DB, not the platform). + +## Development + +This project uses Go 1.26. If you use Nix, a flake is provided to set up the environment. You can load it with `nix develop` or `direnv allow`. + +Available Make commands: +* `make dev`: Runs the development server. +* `make build`: Compiles everything into a single binary. +* `make clean`: Removes the binary. + +When running locally without Docker, the application will create an `app.db` SQLite database file in your current working directory. + +## Docker + +The project includes a Docker setup for those who use it. + +1. Create a `.env` file in the project root. +2. Set the `ROOT_DIR` variable to the directory on your host machine where you want the database to be saved. + +Example `.env`: +ROOT_DIR=/home/user/sprintpadawan_data + +3. Start the container: +docker compose up -d + +The Docker container maps your host `ROOT_DIR` to `/data` inside the container. Sprint Padawan is permanently configured to write its database to `/data` when running in Docker. diff --git a/VERSION.md b/VERSION.md new file mode 100644 index 0000000..6e8bf73 --- /dev/null +++ b/VERSION.md @@ -0,0 +1 @@ +0.1.0 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..09a7b7b --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,9 @@ +services: + app: + build: . + container_name: sprintpadawan + ports: + - "8080:8080" + volumes: + - ${ROOT_DIR:-./data}:/data + restart: unless-stopped diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..3f06416 --- /dev/null +++ b/flake.lock @@ -0,0 +1,61 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1777268161, + "narHash": "sha256-bxrdOn8SCOv8tN4JbTF/TXq7kjo9ag4M+C8yzzIRYbE=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "1c3fe55ad329cbcb28471bb30f05c9827f724c76", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/go.mod b/go.mod index 924d1df..f322ab2 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( require ( github.com/ebitengine/purego v0.9.1 // indirect + github.com/joho/godotenv v1.5.1 // indirect github.com/tursodatabase/turso-go-platform-libs v0.5.3 // indirect golang.org/x/sys v0.43.0 // indirect ) diff --git a/go.sum b/go.sum index 3337fd7..7aca163 100644 --- a/go.sum +++ b/go.sum @@ -4,6 +4,8 @@ github.com/ebitengine/purego v0.9.1 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s github.com/ebitengine/purego v0.9.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= diff --git a/lib/db.go b/lib/db.go index 1286b1e..c1ac68c 100644 --- a/lib/db.go +++ b/lib/db.go @@ -3,16 +3,24 @@ package lib import ( "database/sql" "log" + "os" + "path/filepath" _ "turso.tech/database/tursogo" ) var DB *sql.DB -// init sqlite db — always creates app.db at project root (run from root) +// init sqlite db — creates app.db at project root (run from root) or ROOT_DIR if set func InitDB() { var err error - DB, err = sql.Open("turso", "app.db") + + dbPath := "app.db" + if rootDir := os.Getenv("ROOT_DIR"); rootDir != "" { + dbPath = filepath.Join(rootDir, "app.db") + } + + DB, err = sql.Open("turso", dbPath) if err != nil { log.Fatal(err) } diff --git a/main.go b/main.go index 246794f..fd89e48 100644 --- a/main.go +++ b/main.go @@ -1,25 +1,68 @@ package main import ( + "compress/gzip" "embed" + "io" "log" "net/http" + "strings" + + "github.com/joho/godotenv" "sprintpadawan/api" "sprintpadawan/lib" ) +type gzipResponseWriter struct { + io.Writer + http.ResponseWriter +} + +func (w gzipResponseWriter) Write(b []byte) (int, error) { + return w.Writer.Write(b) +} + +func gzipMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") { + next.ServeHTTP(w, r) + return + } + + w.Header().Set("Content-Encoding", "gzip") + w.Header().Set("Vary", "Accept-Encoding") + + gz := gzip.NewWriter(w) + defer gz.Close() + + gzw := gzipResponseWriter{Writer: gz, ResponseWriter: w} + next.ServeHTTP(gzw, r) + }) +} + +func cacheMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Cache-Control", "public, max-age=31536000") + next.ServeHTTP(w, r) + }) +} + //go:embed static templates var embeddedFiles embed.FS func main() { + // load .env file if it exists + _ = godotenv.Load() + lib.InitDB() api.InitTemplates(embeddedFiles) mux := http.NewServeMux() // serve static assets - mux.Handle("/static/", http.FileServer(http.FS(embeddedFiles))) + staticHandler := http.FileServer(http.FS(embeddedFiles)) + mux.Handle("/static/", cacheMiddleware(gzipMiddleware(staticHandler))) api.SetupRoutes(mux) diff --git a/static/img/favicon.ico b/static/img/favicon.ico new file mode 100644 index 0000000..f06ae95 Binary files /dev/null and b/static/img/favicon.ico differ diff --git a/static/img/logo.png b/static/img/logo.png new file mode 100644 index 0000000..a3d3bfd Binary files /dev/null and b/static/img/logo.png differ diff --git a/static/img/logo.webp b/static/img/logo.webp new file mode 100644 index 0000000..7d827b4 Binary files /dev/null and b/static/img/logo.webp differ diff --git a/templates/layouts.html b/templates/layouts.html index dbb7982..a3abc43 100644 --- a/templates/layouts.html +++ b/templates/layouts.html @@ -3,6 +3,28 @@