From ab6b0b88403bcb1b0639b78621609093b22fd405 Mon Sep 17 00:00:00 2001 From: mkelvers Date: Mon, 6 Apr 2026 07:03:52 +0200 Subject: [PATCH] ops: add dockerfile, makefile and cli tools --- AGENTS.md | 172 ++++++++++++++++++++++++++++++++++++++++ Dockerfile | 33 ++++++++ Makefile | 26 ++++++ cmd/create-user/main.go | 46 +++++++++++ sqlc.yaml | 13 +++ 5 files changed, 290 insertions(+) create mode 100644 AGENTS.md create mode 100644 Dockerfile create mode 100644 Makefile create mode 100644 cmd/create-user/main.go create mode 100644 sqlc.yaml diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..0389bdf --- /dev/null +++ b/AGENTS.md @@ -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 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..4abf870 --- /dev/null +++ b/Dockerfile @@ -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"] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..31508e9 --- /dev/null +++ b/Makefile @@ -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 diff --git a/cmd/create-user/main.go b/cmd/create-user/main.go new file mode 100644 index 0000000..41e459c --- /dev/null +++ b/cmd/create-user/main.go @@ -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) +} diff --git a/sqlc.yaml b/sqlc.yaml new file mode 100644 index 0000000..563aac9 --- /dev/null +++ b/sqlc.yaml @@ -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