ops: add dockerfile, makefile and cli tools
This commit is contained in:
172
AGENTS.md
Normal file
172
AGENTS.md
Normal 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
33
Dockerfile
Normal 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
26
Makefile
Normal 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
46
cmd/create-user/main.go
Normal 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
13
sqlc.yaml
Normal 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
|
||||||
Reference in New Issue
Block a user