# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Additional Instructions
- personal overrides: @~/.claude/CLAUDE.md

## Commands

```bash
# Build
go build -o crowdsec-dashy ./cmd/server

# Production build (static binary, stripped)
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o crowdsec-dashy ./cmd/server

# Run
./crowdsec-dashy

# Lint / vet
go vet ./...

# Tests (none yet — project is in early build phases)
go test ./...

# Docker
docker compose up -d
docker compose build
```

## Architecture

**Go 1.26. Zero external Go dependencies.** Stdlib only (`net/http`, `html/template`, `os/exec`, `encoding/json`).

### Data access — two parallel layers

| Layer | Package | Coverage |
|---|---|---|
| CrowdSec LAPI (REST) | `internal/crowdsec/lapi.go` | Decisions CRUD, Alerts, health |
| `cscli` exec wrapper | `internal/crowdsec/cli.go` | Bouncers, Machines, Hub, Metrics |

All shared structs (Decision, Alert, Bouncer, Machine, HubItem, MetricsSection, etc.) live in `internal/crowdsec/types.go`.

Bouncers, machines, hub, and metrics are **not** exposed via REST — they require `cscli` hitting the database directly. Features that need `cscli` check `Deps.CLIAvailable` and render an inline warning banner when the binary is absent.

### Package layout

```
cmd/server/main.go                entrypoint — loads config, authenticates LAPI, starts HTTP server
internal/config/config.go         env-var loading; CscliAvailable() stat-checks the binary path
internal/crowdsec/
  types.go                        all shared structs
  lapi.go                         REST client (JWT auth, auto-retry on 401)
  cli.go                          cscli exec wrapper (strict allow-list)
internal/handlers/
  renderer.go                     Renderer, PageData, Deps, SidebarNav
  dashboard.go / api.go / ...     one file per page + JSON API handler
internal/middleware/middleware.go  BasicAuth, Logger, SecureHeaders, Recovery
internal/router/router.go         constructs Deps, wires all routes
web/templates/layouts/base.html   sidebar shell
web/templates/pages/*.html        one file per page
web/static/css/app.css            all component styles
web/static/js/dashboard.js        live stat polling + sparklines
web/static/js/tables.js           client-side filter/sort/bulk-select
```

### Request flow

```
HTTP request
  → middleware chain (BasicAuth → Logger → SecureHeaders → Recovery)
  → router.go (stdlib mux)
  → handler struct (receives Deps: Renderer, LAPI, CLI, CLIAvailable, PollInterval)
  → handler renders to bytes.Buffer first, then writes to ResponseWriter
```

### Dependency injection

`Deps` struct (`internal/handlers/renderer.go:246`) is constructed once in `router.New()` and passed to every handler constructor. Handlers are structs, not functions.

### Template system

`Renderer` (`internal/handlers/renderer.go`) parses all page templates at startup: each page in `web/templates/pages/*.html` is combined with `web/templates/layouts/base.html` into a named `*template.Template`. Templates are keyed by filename without extension (e.g. `"dashboard"`, `"decisions"`). All render calls go through `Renderer.Render(w, name, data)` using the buffer-then-write pattern.

Template functions available: `inc`, `dec`, `dict`, `decisionBadgeClass`, `originBadgeClass`, `hubStatusClass`, `safeHTML`, `boolIcon`, `truncate`, `join`.

### LAPI authentication

On startup, `lapi.Login()` POSTs to `/v1/watchers/login` → JWT stored in memory. All subsequent requests carry `Authorization: Bearer <jwt>`. On 401, `doJSON()` auto-retries with a fresh login once before failing.

### `cscli` exec security

All args passed as a slice (never through shell). Two allow-lists enforced before `exec.CommandContext`:
- `allowedSubcommands` (first arg): bouncers, machines, collections, parsers, scenarios, postoverflows, hub, metrics, version
- `allowedActions` (second arg): list, add, delete, install, remove, update, upgrade, validate
- All other user-supplied values validated against `safeArg` regex `^[a-zA-Z0-9_./:@\-]+$`

### POST pattern

All state-changing POST handlers use PRG (Post/Redirect/Get). Flash messages are attached to `PageData` via `WithFlash(type, msg)` and displayed on the redirected GET. All POST handlers must apply `http.MaxBytesReader` before parsing the body.

### Internal JSON API

Used by frontend JS — all return JSON, protected by the same BasicAuth middleware.

| Method | Path | Description |
|---|---|---|
| GET | `/api/v1/stats` | Dashboard summary counts |
| GET | `/api/v1/health` | LAPI health + cscli availability |

### Adding a new page

1. `internal/handlers/mypage.go` — handler struct with `ServeHTTP` or named methods
2. `web/templates/pages/mypage.html` — starts with `{{template "base" .}}`
3. Add `NavItem` to `SidebarNav` in `renderer.go`
4. `router.go` — `mux.Handle("/mypage", handlers.NewMyPageHandler(deps))`

### UI design

Dark industrial aesthetic. Palette: near-black surface `#080b10`, blue-grey panels `#0f1520`, cyan accent `#00d4ff`, threat-red `#ff3b3b`, safe-green `#00e676`. Fonts: `JetBrains Mono` for data, `IBM Plex Sans` for labels. All styles in `web/static/css/app.css`. No external JS frameworks — sparklines drawn with inline SVG `<polyline>`.

Dashboard live stats poll `/api/v1/stats` via `web/static/js/dashboard.js`. Client-side table filter/sort/bulk-select in `web/static/js/tables.js`.

### Environment variables

| Variable | Default | Notes |
|---|---|---|
| `PORT` | `:8080` | Listen address |
| `CROWDSEC_API_URL` | `http://localhost:8080` | LAPI base URL |
| `CROWDSEC_API_LOGIN` | *(required)* | Machine login |
| `CROWDSEC_API_PASSWORD` | *(required)* | Machine password |
| `CSCLI_PATH` | `/usr/local/bin/cscli` | Binary path; CLI features disabled if absent |
| `UI_USERNAME` | `admin` | Basic Auth username |
| `UI_PASSWORD` | `changeme` | Basic Auth password |
| `UI_SESSION_SECRET` | *(required, ≥32 chars)* | HMAC signing key |
| `POLL_INTERVAL_SEC` | `15` | Dashboard poll interval (seconds) |
