First go at it :) #1
@@ -0,0 +1 @@
|
||||
ROOT_DIR=./data
|
||||
+5
-4
@@ -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/
|
||||
|
||||
+21
@@ -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"]
|
||||
@@ -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.
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
0.1.0
|
||||
@@ -0,0 +1,9 @@
|
||||
services:
|
||||
app:
|
||||
build: .
|
||||
container_name: sprintpadawan
|
||||
ports:
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
- ${ROOT_DIR:-./data}:/data
|
||||
restart: unless-stopped
|
||||
Generated
+61
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 219 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 413 KiB |
+55
-11
@@ -3,6 +3,28 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>{{.Title}} — SprintPadawan</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="A lightweight real-time sprint planning tool."
|
||||
/>
|
||||
<meta property="og:title" content="{{.Title}} - SprintPadawan" />
|
||||
<meta
|
||||
property="og:description"
|
||||
content="A lightweight real-time sprint planning tool."
|
||||
/>
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:image" content="/static/img/logo.png" />
|
||||
<meta name="twitter:card" content="summary" />
|
||||
<meta name="twitter:title" content="{{.Title}} - SprintPadawan" />
|
||||
<meta
|
||||
name="twitter:description"
|
||||
content="A lightweight real-time sprint planning tool."
|
||||
/>
|
||||
<meta name="twitter:image" content="/static/img/logo.png" />
|
||||
<meta name="theme-color" content="#6366f1" />
|
||||
<link rel="icon" href="/static/img/favicon.ico" sizes="any" />
|
||||
<link rel="icon" type="image/svg+xml" href="/static/img/logo.webp" />
|
||||
<link rel="apple-touch-icon" href="/static/img/logo.png" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
@@ -16,13 +38,33 @@
|
||||
<script src="/static/js/sse.js" defer></script>
|
||||
{{end}}
|
||||
</head>
|
||||
{{end}}
|
||||
|
||||
{{define "auth-head"}}
|
||||
{{end}} {{define "auth-head"}}
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>{{.Title}} — SprintPadawan</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="A lightweight web application for agile sprint planning."
|
||||
/>
|
||||
<meta property="og:title" content="{{.Title}} — SprintPadawan" />
|
||||
<meta
|
||||
property="og:description"
|
||||
content="A lightweight web application for agile sprint planning."
|
||||
/>
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:image" content="/static/img/logo.png" />
|
||||
<meta name="twitter:card" content="summary" />
|
||||
<meta name="twitter:title" content="{{.Title}} — SprintPadawan" />
|
||||
<meta
|
||||
name="twitter:description"
|
||||
content="A lightweight web application for agile sprint planning."
|
||||
/>
|
||||
<meta name="twitter:image" content="/static/img/logo.png" />
|
||||
<meta name="theme-color" content="#6366f1" />
|
||||
<link rel="icon" href="/static/img/favicon.ico" sizes="any" />
|
||||
<link rel="icon" type="image/svg+xml" href="/static/img/logo.webp" />
|
||||
<link rel="apple-touch-icon" href="/static/img/logo.png" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
@@ -31,16 +73,18 @@
|
||||
/>
|
||||
<link rel="stylesheet" href="/static/styles/main.css" />
|
||||
</head>
|
||||
{{end}}
|
||||
|
||||
{{define "brand-mark"}}
|
||||
{{end}} {{define "brand-mark"}}
|
||||
<a href="/" class="brand-mark">
|
||||
<span class="logo-icon">⚡</span>
|
||||
<span class="logo-text"><span>Sprint</span>Padawan</span>
|
||||
<img
|
||||
src="/static/img/logo.webp"
|
||||
alt="SprintPadawan"
|
||||
height="32"
|
||||
width="32"
|
||||
fetchpriority="high"
|
||||
decoding="async"
|
||||
/>
|
||||
</a>
|
||||
{{end}}
|
||||
|
||||
{{define "session-controls"}}
|
||||
{{end}} {{define "session-controls"}}
|
||||
<div class="topbar-user">
|
||||
<div class="user-avatar">{{slice .Username 0 1}}</div>
|
||||
<span class="topbar-user-name">{{.Username}}</span>
|
||||
|
||||
@@ -4,10 +4,15 @@
|
||||
<body>
|
||||
<div class="auth-card">
|
||||
<div class="auth-logo">
|
||||
<div class="logo-icon">⚡</div>
|
||||
<img
|
||||
src="/static/img/logo.webp"
|
||||
alt="SprintPadawan"
|
||||
height="56"
|
||||
width="56"
|
||||
fetchpriority="high"
|
||||
decoding="async"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<h1 class="auth-title"><span>Sprint</span>Padawan</h1>
|
||||
<p class="auth-subtitle">Sign in to your account</p>
|
||||
|
||||
{{if .Error}}
|
||||
|
||||
@@ -4,10 +4,15 @@
|
||||
<body>
|
||||
<div class="auth-card">
|
||||
<div class="auth-logo">
|
||||
<div class="logo-icon">⚡</div>
|
||||
<img
|
||||
src="/static/img/logo.webp"
|
||||
alt="SprintPadawan"
|
||||
height="56"
|
||||
width="56"
|
||||
fetchpriority="high"
|
||||
decoding="async"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<h1 class="auth-title"><span>Sprint</span>Padawan</h1>
|
||||
<p class="auth-subtitle">Create your account</p>
|
||||
|
||||
{{if .Error}}
|
||||
|
||||
Reference in New Issue
Block a user