First go at it :) #1
@@ -0,0 +1 @@
|
|||||||
|
ROOT_DIR=./data
|
||||||
+5
-4
@@ -1,14 +1,15 @@
|
|||||||
# binary
|
# binary
|
||||||
sprintpadawan
|
sprintpadawan
|
||||||
|
server
|
||||||
|
main
|
||||||
|
|
||||||
# sqlite db
|
# db
|
||||||
app.db
|
app.db
|
||||||
|
app.db-wal
|
||||||
|
|
||||||
# env
|
# env
|
||||||
.env
|
.env
|
||||||
|
|
||||||
# os
|
# os
|
||||||
.DS_Store
|
.DS_Store
|
||||||
Thumbs.db
|
.direnv/
|
||||||
app.db-wal
|
|
||||||
server
|
|
||||||
|
|||||||
+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 (
|
require (
|
||||||
github.com/ebitengine/purego v0.9.1 // indirect
|
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
|
github.com/tursodatabase/turso-go-platform-libs v0.5.3 // indirect
|
||||||
golang.org/x/sys v0.43.0 // 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/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 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
|
|||||||
@@ -3,16 +3,24 @@ package lib
|
|||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"log"
|
"log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
_ "turso.tech/database/tursogo"
|
_ "turso.tech/database/tursogo"
|
||||||
)
|
)
|
||||||
|
|
||||||
var DB *sql.DB
|
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() {
|
func InitDB() {
|
||||||
var err error
|
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 {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +1,68 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"compress/gzip"
|
||||||
"embed"
|
"embed"
|
||||||
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/joho/godotenv"
|
||||||
|
|
||||||
"sprintpadawan/api"
|
"sprintpadawan/api"
|
||||||
"sprintpadawan/lib"
|
"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
|
//go:embed static templates
|
||||||
var embeddedFiles embed.FS
|
var embeddedFiles embed.FS
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
// load .env file if it exists
|
||||||
|
_ = godotenv.Load()
|
||||||
|
|
||||||
lib.InitDB()
|
lib.InitDB()
|
||||||
api.InitTemplates(embeddedFiles)
|
api.InitTemplates(embeddedFiles)
|
||||||
|
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
|
|
||||||
// serve static assets
|
// 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)
|
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 charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>{{.Title}} — SprintPadawan</title>
|
<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.googleapis.com" />
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
<link
|
<link
|
||||||
@@ -16,13 +38,33 @@
|
|||||||
<script src="/static/js/sse.js" defer></script>
|
<script src="/static/js/sse.js" defer></script>
|
||||||
{{end}}
|
{{end}}
|
||||||
</head>
|
</head>
|
||||||
{{end}}
|
{{end}} {{define "auth-head"}}
|
||||||
|
|
||||||
{{define "auth-head"}}
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>{{.Title}} — SprintPadawan</title>
|
<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.googleapis.com" />
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
<link
|
<link
|
||||||
@@ -31,16 +73,18 @@
|
|||||||
/>
|
/>
|
||||||
<link rel="stylesheet" href="/static/styles/main.css" />
|
<link rel="stylesheet" href="/static/styles/main.css" />
|
||||||
</head>
|
</head>
|
||||||
{{end}}
|
{{end}} {{define "brand-mark"}}
|
||||||
|
|
||||||
{{define "brand-mark"}}
|
|
||||||
<a href="/" class="brand-mark">
|
<a href="/" class="brand-mark">
|
||||||
<span class="logo-icon">⚡</span>
|
<img
|
||||||
<span class="logo-text"><span>Sprint</span>Padawan</span>
|
src="/static/img/logo.webp"
|
||||||
|
alt="SprintPadawan"
|
||||||
|
height="32"
|
||||||
|
width="32"
|
||||||
|
fetchpriority="high"
|
||||||
|
decoding="async"
|
||||||
|
/>
|
||||||
</a>
|
</a>
|
||||||
{{end}}
|
{{end}} {{define "session-controls"}}
|
||||||
|
|
||||||
{{define "session-controls"}}
|
|
||||||
<div class="topbar-user">
|
<div class="topbar-user">
|
||||||
<div class="user-avatar">{{slice .Username 0 1}}</div>
|
<div class="user-avatar">{{slice .Username 0 1}}</div>
|
||||||
<span class="topbar-user-name">{{.Username}}</span>
|
<span class="topbar-user-name">{{.Username}}</span>
|
||||||
|
|||||||
@@ -4,10 +4,15 @@
|
|||||||
<body>
|
<body>
|
||||||
<div class="auth-card">
|
<div class="auth-card">
|
||||||
<div class="auth-logo">
|
<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>
|
</div>
|
||||||
|
|
||||||
<h1 class="auth-title"><span>Sprint</span>Padawan</h1>
|
|
||||||
<p class="auth-subtitle">Sign in to your account</p>
|
<p class="auth-subtitle">Sign in to your account</p>
|
||||||
|
|
||||||
{{if .Error}}
|
{{if .Error}}
|
||||||
|
|||||||
@@ -4,10 +4,15 @@
|
|||||||
<body>
|
<body>
|
||||||
<div class="auth-card">
|
<div class="auth-card">
|
||||||
<div class="auth-logo">
|
<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>
|
</div>
|
||||||
|
|
||||||
<h1 class="auth-title"><span>Sprint</span>Padawan</h1>
|
|
||||||
<p class="auth-subtitle">Create your account</p>
|
<p class="auth-subtitle">Create your account</p>
|
||||||
|
|
||||||
{{if .Error}}
|
{{if .Error}}
|
||||||
|
|||||||
Reference in New Issue
Block a user