ops: add dockerfile, makefile and cli tools

This commit is contained in:
2026-04-06 07:03:52 +02:00
parent 1ab6f57aa4
commit ab6b0b8840
5 changed files with 290 additions and 0 deletions

172
AGENTS.md Normal file
View File

@@ -0,0 +1,172 @@
# malago
personal anime tracking platform. go/htmx rewrite.
## stack
- go standard library (`net/http`)
- htmx + templ templates
- sqlite + sqlc
- tailwind (dark theme)
- jikan api (myanimelist)
## structure
```
cmd/server/ main entry
internal/
auth/ sessions, passwords
database/ sqlc generated, migrations
handlers/ http handlers by domain
middleware/ auth, logging
jikan/ api client
templates/ templ components
migrations/ sql files
static/ css, js
```
## go patterns
### errors
always handle errors explicitly. wrap with context using `fmt.Errorf`:
```go
if err != nil {
return fmt.Errorf("failed to fetch user: %w", err)
}
```
use `errors.Is` and `errors.As` to check wrapped errors.
### early returns
reduce nesting. check errors first, return early:
```go
func getUser(id string) (*User, error) {
if id == "" {
return nil, ErrInvalidID
}
user, err := db.FindUser(id)
if err != nil {
return nil, err
}
return user, nil
}
```
### defer for cleanup
always close resources with defer:
```go
resp, err := http.Get(url)
if err != nil {
return err
}
defer resp.Body.Close()
```
### interfaces
accept interfaces, return structs. keep interfaces small:
```go
type Reader interface {
Read(p []byte) (n int, err error)
}
```
### naming
- short, lowercase package names: `auth`, `jikan`, `db`
- `MixedCaps` for exports, `mixedCaps` for internal
- getters: `Owner()` not `GetOwner()`
- interfaces: single method = method name + `er` suffix (`Reader`, `Writer`)
### zero values
design structs so zero value is useful:
```go
var buf bytes.Buffer // ready to use, no init needed
buf.WriteString("hello")
```
### composition over inheritance
embed types to compose behavior:
```go
type Handler struct {
db *database.Queries
jikan *jikan.Client
logger *slog.Logger
}
```
## htmx
check for htmx requests:
```go
func isHTMX(r *http.Request) bool {
return r.Header.Get("HX-Request") == "true"
}
```
return partials for htmx, full pages otherwise. use `hx-swap-oob` for multiple updates. trigger toasts with `HX-Trigger` header.
## templ
render components directly to response:
```go
func (h *Handler) Home(w http.ResponseWriter, r *http.Request) {
templates.HomePage(data).Render(r.Context(), w)
}
```
pass data explicitly. keep components focused. use layouts.
## database
sqlc generates type-safe queries. always use parameterized queries.
tables: `user`, `session`, `account`, `anime`, `watch_list_entry`
watch statuses: `watching`, `completed`, `on_hold`, `dropped`, `plan_to_watch`
## jikan api
rate limit: 3 req/sec max. stagger batch requests. cache in local db.
## commands
```bash
make dev # hot reload (air)
make build # binary
make test # tests
make migrate # run migrations
make sqlc # generate code
make create-user # cli user creation
```
## env
```bash
DATABASE_FILE=malago.db
SESSION_SECRET=min_32_chars_random
PORT=3000
```
## avoid
- panics in handlers
- forgetting `defer resp.Body.Close()`
- unstaggered jikan requests (429 errors)
- globals for config/state
- large monolithic templates

33
Dockerfile Normal file
View File

@@ -0,0 +1,33 @@
FROM golang:1.22-bullseye AS builder
WORKDIR /app
# Enable CGO for sqlite3
ENV CGO_ENABLED=1
COPY go.mod go.sum ./
RUN go mod download
COPY . .
# Build the server and the CLI tools
RUN go build -o main_server ./cmd/server
RUN go build -o create_user ./cmd/create-user
FROM debian:bullseye-slim
WORKDIR /app
# Required for sqlite
RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/*
COPY --from=builder /app/main_server .
COPY --from=builder /app/create_user .
COPY --from=builder /app/static ./static
COPY --from=builder /app/migrations ./migrations
# Expose the application port
EXPOSE 3000
# Run the server
CMD ["./main_server"]

26
Makefile Normal file
View File

@@ -0,0 +1,26 @@
.PHONY: dev build test migrate sqlc create-user
dev:
air
build:
go build -o main_server ./cmd/server
test:
go test ./...
migrate:
sqlite3 malago.db < migrations/001_init.sql
sqlc:
sqlc generate
templ:
templ generate
create-user:
@if [ -z "$(EMAIL)" ] || [ -z "$(PASSWORD)" ]; then \
echo "Usage: make create-user EMAIL=your@email.com PASSWORD=yourpassword"; \
else \
go run ./cmd/create-user -email=$(EMAIL) -password=$(PASSWORD); \
fi

46
cmd/create-user/main.go Normal file
View File

@@ -0,0 +1,46 @@
package main
import (
"context"
"database/sql"
"flag"
"fmt"
"log"
"os"
_ "github.com/mattn/go-sqlite3"
"malago/internal/auth"
"malago/internal/database"
)
func main() {
email := flag.String("email", "", "Email/Username for the new user")
password := flag.String("password", "", "Password for the new user")
flag.Parse()
if *email == "" || *password == "" {
fmt.Println("Usage: make create-user EMAIL=user@example.com PASSWORD=secret")
fmt.Println("Or : go run ./cmd/create-user -email=user@example.com -password=secret")
os.Exit(1)
}
db, err := sql.Open("sqlite3", "malago.db")
if err != nil {
log.Fatalf("failed to open db: %v", err)
}
defer db.Close()
queries := database.New(db)
authService := auth.NewService(queries)
ctx := context.Background()
// Try to create the user
user, err := authService.RegisterUser(ctx, *email, *password)
if err != nil {
log.Fatalf("Failed to create user: %v", err)
}
fmt.Printf("Successfully created user: %s (ID: %s)\n", user.Username, user.ID)
}

13
sqlc.yaml Normal file
View File

@@ -0,0 +1,13 @@
version: "2"
sql:
- engine: "sqlite"
queries: "internal/database/queries.sql"
schema: "migrations/"
gen:
go:
package: "database"
out: "internal/database"
emit_json_tags: true
emit_prepared_queries: false
emit_interface: true
emit_exact_table_names: false