all repos — weimar @ cd624b1b90061a10147950a7633dd7579c78a61d

Unnamed repository; edit this file 'description' to name the repository.

AGENTS.md (view raw)

  1# weimar — Agent Guide
  2
  3Single-binary media repository for home servers. Go backend with embedded SvelteKit SPA.
  4
  5## Essential Commands
  6
  7| Command | Purpose |
  8|---------|---------|
  9| `make build` | Full build: SvelteKit → embed → Go binary (output: `bin/weimar`) |
 10| `make test` | `go test ./... -count=1` (disables cache) |
 11| `make lint` | `golangci-lint run ./...` (v2 config) |
 12| `make web-dev` | SvelteKit dev server (Vite) |
 13| `make web-build` | SvelteKit production build only |
 14| `make web-embed` | Build SvelteKit + copy into `cmd/weimar/web/build/` for embedding |
 15| `make clean` | Remove all build artifacts |
 16| `go vet ./...` | Passes clean |
 17
 18See full Makefile for details.
 19
 20## Build Flow
 21
 22```
 23web/ (SvelteKit)
 24  → bun run build → web/build/
 25  → cp -r → cmd/weimar/web/build/
 26  → go build with //go:embed web/build/* → single binary
 27```
 28
 29The SvelteKit build is copied into the Go module so `embed.FS` can include it. **If you modify Svelte files, run `make web-embed` before building the Go binary**, or `make build` does both steps.
 30
 31## Code Organization
 32
 33```
 34cmd/weimar/           # Cobra CLI entry point
 35├── main.go           # os.Exit on Execute() error
 36├── root.go           # Persistent flags, viper config init
 37├── serve.go          # Server command + //go:embed web/build/*
 38├── users.go          # Users subcommands (list/create/delete/password-reset)
 39├── image.go          # Image subcommands (delete)
 40└── version.go        # Version command
 41
 42internal/
 43├── config/config.go  # FromViper() reads typed Config struct from viper
 44├── server/
 45│   ├── server.go     # HTTP server, route registration, SPA handler
 46│   └── middleware.go # requireAuth wrapper, writeJSONError
 47├── api/
 48│   ├── api.go        # API struct (dependency holder), writeJSON/writeError
 49│   ├── auth.go       # Login/Register/Logout handlers
 50│   ├── upload.go     # File upload + processing pipeline
 51│   ├── images.go     # List/Get image handlers
 52│   ├── download.go   # Serve original file
 53│   ├── thumbnail.go  # Serve thumbnail
 54│   └── tags.go       # Tag autocomplete handler
 55├── db/
 56│   ├── models.go     # User, Image, Session, Tag structs
 57│   ├── sqlite.go     # DB open, close, migration system
 58│   └── queries.go    # All SQL CRUD operations
 59├── auth/
 60│   ├── session.go    # SessionManager: login, authenticate, logout
 61│   └── register.go   # Register function with validation
 62├── storage/
 63│   ├── disk.go       # Hash-partitioned file storage
 64│   └── processing.go # MIME detection, EXIF strip, thumbnails
 65└── config/config.go  # Typed config struct (read from viper)
 66
 67web/                  # SvelteKit SPA (adapter-static, CSR-only)
 68├── src/
 69│   ├── routes/
 70│   │   ├── +layout.svelte     # Nav bar, auth check (getMe), logout
 71│   │   ├── +layout.js         # ssr=false, prerender=false
 72│   │   ├── +page.svelte       # Home page
 73│   │   ├── browse/+page.svelte  # Gallery grid (6-col, tags, pagination, viewer)
 74│   │   ├── login/+page.svelte   # Login/Register form
 75│   │   ├── upload/+page.svelte  # File upload + tag autocomplete
 76│   │   ├── settings/+page.svelte  # Avatar upload, user info
 77│   │   └── image/[id]/+page.svelte  # Image detail + download
 78│   └── lib/
 79│       ├── api.ts             # Fetch wrapper, typed API functions
 80│       ├── Avatar.svelte      # Avatar component (image or SVG fallback)
 81│       └── ImageViewer.svelte # Full-screen overlay viewer
 82```
 83
 84## Architecture
 85
 86### Backend (Go)
 87- **Router**: Go 1.22+ `http.ServeMux` with method+pattern routing (e.g., `"GET /api/images/{id}"`)
 88- **Config layering**: CLI flags > env vars (`WEIMAR_*`) > TOML file > defaults (set in `root.go:setDefaults()`)
 89- **Database**: SQLite via `modernc.org/sqlite` (pure Go, no CGO). Single connection (`MaxOpenConns(1)`)
 90- **Auth**: Cookie-based. Sessions stored in SQLite, 30-day sliding expiry, 64-char hex tokens
 91- **Storage**: Hash-partitioned (`sha256[:2]/[2:4]/[4:6]/[:12].ext`), prevents directory overcrowding
 92- **Error handling**: Sentinel errors in auth package, wrapped with `%w`, switch on sentinel in handlers
 93
 94### Frontend (SvelteKit)
 95- **Svelte 5** with runes (`$state`, `$derived`, `$props`)
 96- **CSR-only SPA** — `+layout.js` sets `ssr=false, prerender=false`
 97- **Auth state**: Checked in layout by `GET /api/users/me` via `getMe()`
 98- **API layer**: `web/src/lib/api.ts` — thin fetch wrapper with `credentials: 'include'`, all paths prefixed `/api`
 99- **No Node.js runtime in production** — embedded static files only
100
101### Request Flow
102```
103Browser → SvelteKit SPA → fetch('/api/...') → Go ServeMux
104  → requireAuth middleware (reads weimar_session cookie)
105  → API handler → DB/storage → JSON response → SPA renders
106```
107
108## Testing Patterns
109
110- **`:memory:` SQLite** for all tests — fast, isolated
111- **`t.TempDir()`** for temp storage — auto-cleaned
112- **`t.Cleanup(func() { db.Close() })`** for resource cleanup
113- **`t.Helper()`** on all setup/helper functions
114- **`setupTest(t)`** in `api_test.go` — creates DB, migrates, registers+logs in a test user, returns API + token
115- **`authenticatedRequest(t, ...)`** — creates `httptest.NewRequest` with session cookie + `CtxKeyUser` context
116- **`newTestDB(t)`** in `db_test.go` — opens `:memory:`, migrates
117- **`setupTestDB(t)`** in `auth_test.go` — same for auth package
118- Tests use table-driven style with descriptive test names
119- No external test dependencies; no test fixtures or golden files
120- **Go nil slices marshal to JSON `null`**: Always use `make([]T, 0)` instead of `var s []T` for API-facing responses — the frontend can't call `.length` on `null`.
121- **`r.FormValue()` returns first value only**: When a form field appears multiple times (e.g., `tags=foo&tags=bar` from frontend `FormData.append`), `FormValue` returns only the first. Use `r.Form["tags"]` to get all values as `[]string`.
122
123## Naming & Conventions
124
125| Pattern | Where | Example |
126|---------|-------|---------|
127| `scanXxx()` | `db/queries.go` | `scanUser`, `scanSession`, `scanImage` |
128| `writeJSON(w, status, v)` | `api/api.go` | All API handlers |
129| `writeError(w, status, msg)` | `api/api.go` | Error responses |
130| `UserFromContext(ctx)` | `api/api.go` | Extract authenticated user |
131| `now()` | `db/sqlite.go` | RFC 3339 UTC timestamp |
132| `scanTime(s)` | `db/sqlite.go` | Parse RFC 3339 string |
133| Sentinel errors | `auth/` | `ErrInvalidCredentials`, `ErrUsernameTaken` |
134| `CtxKeyUser` | `api/api.go` | Context key for authenticated user |
135| `itoa(n)` | `server/server.go` | Custom int→string (avoids strconv import) |
136
137## API Endpoints
138
139| Method | Path | Auth | Notes |
140|--------|------|------|-------|
141| `GET` | `/api/health` | No | `{"status":"ok"}` |
142| `POST` | `/api/auth/login` | No | Returns `weimar_session` cookie |
143| `POST` | `/api/auth/register` | No | Gated by `auth.allow_registration` config, auto-logs in |
144| `POST` | `/api/auth/logout` | Yes | Clears session |
145| `POST` | `/api/upload` | Yes | Multipart form: `file` + `tags` |
146| `GET` | `/api/images` | No* | Query: `?tags=...&page=1&per_page=60` *public unless `auth.require_auth_for_browse=true` |
147| `GET` | `/api/images/{id}` | No* | Full metadata (incl. tags_detail, uploader info, is_owner) |
148| `GET` | `/api/images/{id}/download` | No* | Streams original file |
149| `GET` | `/api/images/{id}/thumbnail` | No* | JPEG thumbnail (300px max) |
150| `GET` | `/api/tags/suggest?q=` | Yes | LIKE prefix match, ordered by usage |
151| `POST` | `/api/images/{id}/tags` | Yes | Add tags (owner only) |
152| `DELETE` | `/api/images/{id}/tags` | Yes | Remove tags (owner only) |
153| `GET` | `/api/users/me` | Yes | Current user (username, avatar_url, is_admin) |
154| `POST` | `/api/users/me/avatar` | Yes | Upload avatar (128px JPEG) |
155| `GET` | `/api/users/{id}/avatar` | No | Serve avatar image |
156
157## Gotchas & Non-Obvious Patterns
158
1591. **Embed quirk**: `//go:embed web/build/*` in `serve.go` — gopls reports a stale `no matching files found` error until the SvelteKit build exists. The build compiles fine; this is a gopls caching issue.
160
1612. **Build order**: If you change Svelte files, you must run `make web-embed` before `go build`. Running `make build` handles this.
162
1633. **Session cookie**: Named `weimar_session` (constant in `auth/session.go`). `HttpOnly`, `SameSite=Lax`, no `Secure` flag (HTTP home server). Extended by 30 days on **every** authenticated request.
164
1654. **Auth check in SPA**: The layout calls `GET /api/users/me` via `getMe()` to determine login status and fetch user info.
166
1675. **Thumbnails**: Always JPEG quality 80. Videos depend on `ffmpeg` being on `$PATH` — if missing, `thumbnail_path` stays null and Svelte shows "No thumbnail".
168
1696. **MIME detection**: By magic bytes, never file extension. Triggered in `upload.go` before any processing.
170
1717. **EXIF stripping**: JPEG only. Skips APP1 segments without recompression. Other formats pass through unchanged.
172
1738. **Tag normalization**: `normalizeTag()` trims, lowercases, collapses internal whitespace. `"  Funny  Cat "``"funny cat"`.
174
1759. **Tag filter is AND**: Multiple `?tags=` query params require the image to have all specified tags (via `HAVING COUNT = N`).
176
17710. **Tag autocomplete**: From materialized `tags` table, not live query against `image_tags`. Updated on upload/tag change.
178
17911. **SQLite safety**: `MaxOpenConns(1)` — SQLite doesn't handle concurrent writes from multiple connections. This is intentional.
180
18112. **Password reset**: CLI command reads password from stdin (not arg), validates minimum length, hashes with bcrypt cost 10.
182
18313. **`app.html` linter warnings**: `%sveltekit.head%` and `%sveltekit.body%` cause false-positive HTML parser errors. These are correct SvelteKit template placeholders.
184
18514. **GoReleaser**: v2 config with UPX compression (level 9) for linux/darwin amd64/arm64. CGO_ENABLED=0. Windows disabled.
186
18715. **Golangci-lint**: v2 config with 12 specific linters. No `gofmt` or `revive` — relies on specific checks.
188
189## CLI Admin Commands
190
191```
192weimar serve --config weimar.toml --admin-email admin@example.com --admin-password s3cur3
193weimar users list
194weimar users create <username> <password> [--admin]
195weimar users delete <username>
196weimar users password-reset <username>
197weimar image delete <id>
198```
199
200All commands support `--config` flag (defaults to `./weimar.toml`) or `WEIMAR_CONFIG` env var.
201
202## Config Structure (TOML)
203
204```toml
205[server]
206host = "0.0.0.0"
207port = 8080
208# behind_proxy = true   # set when behind Caddy/nginx (enables Secure cookies)
209# site_title = "My Site"  # shown in nav and page title (default "weimar")
210
211[database]
212path = "./data/weimar.db"
213
214[storage]
215path = "./data/images"
216
217[upload]
218max_size_mb = 50
219rate_limit_per_minute = 0
220
221[auth]
222allow_registration = true
223require_auth_for_browse = false
224```
225
226All fields have defaults (see `root.go:setDefaults()`). Config file is optional.