claude-context.md

185 lines
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185
# Congo Project

Built with the Congo framework — Go + HTMX + DaisyUI + React islands.

## Commands

```bash
congo dev         # development server (ENV=development, in-memory DB)
congo build       # production binary
congo launch      # deploy to remote server
congo new <name>  # add a new app to the project
congo connect     # SSH into deployed server
congo destroy     # tear down infrastructure
congo claude      # AI-assisted development
congo status      # check deployment health
congo logs        # stream service logs
congo source      # extract flat buildable copy of framework
```

## Architecture

MVC with HTMX-first server rendering. React islands for complex interactive UI.

```
controllers/    Route handlers + template methods
models/         Database models with auto-migration ORM
views/          Go HTML templates (HTMX + DaisyUI)
components/     React island components (optional)
internal/       Vendored Congo framework (modifiable)
```

## Controller Pattern

```go
// Factory function returns (name, controller).
func Todos() (string, *TodosController) {
    return "todos", &TodosController{}
}

type TodosController struct {
    application.BaseController  // Always embed
}

func (c *TodosController) Setup(app *application.App) {
    c.BaseController.Setup(app)
    http.Handle("GET /todos", app.Serve("todos.html", nil))
    http.Handle("POST /todos", app.Method(c, "Create", nil))
    http.Handle("POST /todos/{id}/delete", app.Method(c, "Delete", nil))
}

// VALUE receiver — creates copy for request isolation.
func (c TodosController) Handle(r *http.Request) application.Controller {
    c.Request = r
    return &c
}

// Template methods (pointer receiver) — accessible as {{todos.All}}.
func (c *TodosController) All() []*models.Todo {
    todos, _ := models.Todos.Search("ORDER BY CreatedAt DESC")
    return todos
}

// POST handler — create.
func (c *TodosController) Create(w http.ResponseWriter, r *http.Request) {
    todo := &models.Todo{Title: r.FormValue("title")}
    if _, err := models.Todos.Insert(todo); err != nil {
        c.RenderError(w, r, err)
        return
    }
    c.Refresh(w, r) // reload current page
}

// POST handler — delete.
func (c *TodosController) Delete(w http.ResponseWriter, r *http.Request) {
    if err := models.Todos.DeleteByID(r.PathValue("id")); err != nil {
        c.RenderError(w, r, err)
        return
    }
    c.Redirect(w, r, "/todos") // navigate to different URL
}
```

Register in `main.go`:
```go
application.WithController(controllers.Todos()),
```

## Model Pattern

```go
type Todo struct {
    database.Model  // ID (string UUID), CreatedAt, UpdatedAt
    Title string
    Done  bool
}

var Todos = database.Manage(DB, new(Todo),
    database.WithIndex[Todo]("Title"),
)
```

Database init in `models/db.go`:
```go
var DB = engines.NewAuto()  // env-based: DB_URL+DB_TOKEN=remote, DB_PATH=local, neither=memory
```

Collection methods: `Get(id)`, `First(where, args...)`, `Search(where, args...)`, `All()`, `Insert(entity)`, `Update(entity)`, `Delete(entity)`, `DeleteByID(id)`, `Count(where, args...)`.

Query examples:
```go
models.Todos.Search("WHERE Done = ? ORDER BY CreatedAt DESC LIMIT 10", false)
models.Todos.First("WHERE Title = ?", "Buy milk")
models.Todos.Count("WHERE Done = ?", true)
```

## View + HTMX Pattern

```html
{{template "main.html" .}}

{{define "title"}}Todos{{end}}

{{define "content"}}
<div class="container mx-auto p-8">
    {{range $todo := todos.All}}
    <div class="card bg-base-100 mb-2 p-4 flex justify-between items-center">
        <span>{{$todo.Title}}</span>
        <button hx-post="/todos/{{$todo.ID}}/delete" hx-target="body" class="btn btn-sm btn-error">
            Delete
        </button>
    </div>
    {{end}}

    <form hx-post="/todos" hx-target="body" class="flex gap-2 mt-4">
        <input name="title" class="input input-bordered flex-1" required />
        <button class="btn btn-primary">Add</button>
    </form>
</div>
{{end}}
```

## Key Conventions

- **IDs are ALWAYS strings** (UUIDs). Never convert to int.
- **SQL uses PascalCase**: `WHERE UserID = ?` not `WHERE user_id = ?`
- **Templates by filename only**: `{{template "nav.html" .}}` not `"partials/nav.html"`
- **Render partial**: `c.Render(w, r, "partial.html", data)` in POST handlers
- **Error handling**: `c.RenderError(w, r, err)` returns 200 OK with error HTML for HTMX
- **Redirect**: `c.Redirect(w, r, "/path")` — sends HX-Location header for HTMX, 303 for regular requests
- **Refresh**: `c.Refresh(w, r)` — sends HX-Refresh header for HTMX, 303 redirect-to-self for regular requests
- **CSRF**: HTMX + SameSite=Lax cookies provides CSRF protection. No tokens needed.
- **PathValue**: in handlers `r.PathValue("id")`, in template methods `c.PathValue("id")`

## Application Options

```go
application.WithValue("key", value)         // template function returning value
application.WithMiddleware(mw)              // add middleware to handler chain
application.WithHealthPath("/healthz")      // custom health endpoint (default: /health)
application.WithController(controllers.X()) // register controller
application.WithFunc("name", fn)            // register template function
```

## Go Style

- Package-level vars as runtime state (no config structs)
- `cmp.Or()` for env var defaults (Go 1.22+)
- No custom ServeMux — use `http.DefaultServeMux`
- No single-line wrapper methods around stdlib calls
- Functions exist for reuse or naming — inline single-use logic

## Frontend Islands (when present)

React components in `components/`, mounted via `{{render "ComponentName" props}}` in templates. Auto-mounted on page load and HTMX swaps. HMR in development mode.

```html
{{render "Counter" home.CounterProps}}
```

Controller provides props:
```go
func (c *HomeController) CounterProps() map[string]any {
    return map[string]any{"initial": 0, "label": "Clicks"}
}
```