diff options
| author | Paul Buetow <paul@buetow.org> | 2026-04-29 20:31:40 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-04-29 20:31:40 +0300 |
| commit | 5360e89457b057eb4faff03d995116ffaef4d543 (patch) | |
| tree | 6646e825ee0164d2ac22d5dba9b2af040dc58153 | |
| parent | 4a70c0ea083306cb79d0fa325d331ffa4333154a (diff) | |
docs: create AGENTS.md documentation
| -rw-r--r-- | AGENTS.md | 265 | ||||
| -rw-r--r-- | PLAN.md | 22 |
2 files changed, 276 insertions, 11 deletions
diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..b558dd9 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,265 @@ +# KISS Media Player — Agent Documentation + +This file is written for coding agents working on the `kiss-media-player` project. + +--- + +## Architecture Overview + +The project is a **self-hosted media player** designed for simplicity (KISS): minimal dependencies, no frontend frameworks, interface-driven Go code for easy testing. + +### Backend + +- **Language / Runtime:** Go 1.23 +- **HTTP Server:** `net/http` stdlib only; `http.ServeMux` with pattern matching +- **Database:** SQLite via `modernc.org/sqlite` +- **Media Processing:** `ffmpeg` / `ffprobe` installed in runtime container +- **Password Hashing:** `golang.org/x/crypto/bcrypt` + +**Layered architecture:** + +| Layer | Package | Role | +|-------|---------|------| +| Entrypoint | `cmd/mediaplayer` | Flags, config, dependency wiring, server start | +| API / Transport | `internal/api` | `Server` struct holds `http.ServeMux`, route table, middleware, handlers | +| Service | `internal/service` | Business logic: `MediaService`, `AdminService`, `ProgressService`, `GCWorker` | +| Repository | `internal/repository` | `Store` interface (composite of per-entity repos); concrete SQLite in `sqlite.go` | +| Domain | `internal/model` | Pure structs (`Media`, `User`, `Set`, `Session`, etc.) — zero external deps | +| Utilities | `internal/auth`, `internal/scanner`, `internal/probe`, `internal/thumb`, `internal/clock`, `internal/setassign` | Hasher, session manager, filesystem scanner, ffprobe wrapper, thumbnail generator, clock abstraction, permission helper | + +All external dependencies are injected via constructors (e.g., `NewServer`, `NewMediaService`, `NewFSScanner`). Hand-written mocks live in `internal/repository/mock.go` and `internal/service/mock.go`. + +### Frontend + +- **Stack:** Vanilla ES modules, no bundler or build step +- **Pages:** `index.html` (SPA), `login.html`, `bootstrap.html` +- **Styling:** CSS Custom Properties (`var(--*)`) defined in `web/css/theme.css`; utility/component/layout styles in separate files +- **PWA:** `manifest.json` + `web/sw.js` caches static assets for offline usage +- **Media Player:** HTML5 `<video>` / `<audio>` with a custom overlay control bar +- **Routing:** Simple page-type switch in `app.js` based on `location.pathname` + +### Build & Deploy + +- **Build Tool:** Mage (`Magefile.go`) +- **Container:** Multi-stage `Dockerfile` (Go builder → Alpine runtime with `ffmpeg`) +- **Kubernetes:** `Deployment` + `Service` + two PVCs (`/data` for DB, `/media` for library) + +--- + +## Running Tests + +```bash +# Run all tests with race detector and coverage +go test ./... -race -cover + +# Generate coverage profile and view it +go test ./... -race -coverprofile=coverage.out && go tool cover -func=coverage.out +``` + +Tests use: +- `:memory:` SQLite instances for repository-layer tests +- Hand-written mocks (fakes) for service-layer tests +- `httptest` + mocked services for handler tests +- Golden JSON fixtures for `probe` tests + +--- + +## Mage Targets + +Install `mage` if you don't have it already: + +```bash +go install github.com/magefile/mage@latest +``` + +Available targets (from `Magefile.go`): + +| Target | Description | +|--------|-------------| +| `mage` (default) | Same as `mage build` | +| `mage build` | Compile the binary (`go build -o play ./cmd/mediaplayer`) | +| `mage test` | Run `go test ./...` | +| `mage install` | Build and copy `play` to `$GOPATH/bin` (or `~/go/bin`) | +| `mage clean` | Remove the `play` binary | +| `mage docker-build` | Build container image as `kiss-media-player:latest` | +| `mage docker-push` | Push `kiss-media-player:latest` to registry | + +--- + +## Kubernetes Deployment + +The `k8s/` directory contains: + +| File | Resource | +|------|----------| +| `k8s/deployment.yaml` | `Deployment` (1 replica, non-root `65534:65534`, probes) | +| `k8s/service.yaml` | `ClusterIP` Service on port 8080 | +| `k8s/pvc-db.yaml` | `PersistentVolumeClaim` (`ReadWriteOnce`, 1Gi) for `/data` | +| `k8s/pvc-media.yaml` | `PersistentVolumeClaim` (`ReadWriteMany`, 10Gi) for `/media` | +| `k8s/secret.yaml` | `Secret` (optional) for environment overrides (e.g., `ADMIN_PASSWORD`) | + +Deploy everything: + +```bash +kubectl apply -f k8s/ +``` + +The `Deployment` overrides two critical settings for K8s: +- `DB_PATH=/data/media.db` +- `MEDIA_ROOT=/media` + +Probes: +- **Liveness:** `GET /healthz` (no DB dependency) +- **Readiness:** `GET /readyz` (DB ping) + +Security: +- `runAsNonRoot: true` +- `runAsUser: 65534` / `runAsGroup: 65534` +- `allowPrivilegeEscalation: false` +- `readOnlyRootFilesystem: true` + +--- + +## Theming Guide + +All colors live in `web/css/theme.css` as CSS Custom Properties on `:root`. + +### Current Implementation + +`themes.js` swaps the active theme by setting `document.documentElement.setAttribute('data-theme', ...)` and saves the preference to `localStorage`. Override blocks in `theme.css` handle the light variant: + +```css +/* Default (dark) — defined on :root */ +:root { + --bg-body: #0f1117; + --text-primary: #e6e8ef; + --accent: #5e9eff; + ... +} + +/* Light theme overrides */ +[data-theme="light"] { + --bg-body: #f4f5f8; + --text-primary: #12131a; + --accent: #2b6cb0; + ... +} +``` + +### Adding a New Theme + +Option A — inline override (recommended for small additions): + +1. Open `web/css/theme.css`. +2. Append a new attribute selector after the light block, e.g.: + +```css +[data-theme="solarized"] { + --bg-body: #002b36; + --text-primary: #839496; + --accent: #268bd2; + ... +} +``` + +3. Wire the toggle in `web/js/themes.js` (or expose a selector UI in `index.html`) to call `apply('solarized')`. + +Option B — separate file (if you prefer a stylesheet swap): + +1. Create `web/css/themes/<name>.css` containing `:root { ... }` overrides. +2. Dynamically create or swap a `<link rel="stylesheet">` in `themes.js` instead of using `data-theme`. + +**Rules:** +- No color literals in component styles — everything must go through `var(--*)`. +- Do not add inline styles in HTML or JS. + +--- + +## Keyboard Shortcuts + +Global shortcuts are registered in `web/js/keyboard.js`. They are **disabled** while the user is focused on an `INPUT`, `TEXTAREA`, or `contentEditable` element (except `Escape` to blur). + +| Key | Action | +|-----|--------| +| `↑` / `↓` | Navigate media list (up / down) | +| `k` / `j` | Navigate media list (up / down) | +| `←` / `→` | Switch sets / pages | +| `h` / `l` | Switch sets / pages | +| `Enter` | Open selected media (navigate to detail) | +| `Space` / `p` | Play / pause / switch to selected item | +| `f` | Toggle fullscreen on the player wrapper | +| `Esc` | Exit fullscreen, or deselect current item | +| `r` | Toggle shuffle on the current filtered result set | +| `s` | Generate a share link for the selected media | +| `/` | Focus the quick search bar (debounced) | +| `n` | Open notes modal for the selected media | + +--- + +## Admin Tasks + +Admin endpoints are gated by `RequireAdmin` middleware (checks `users.is_admin`). The admin panel is opened via the hidden "Admin" button in the SPA header (shown only when the current user is an admin). + +### Creating Users + +1. Open the admin panel. +2. Enter username, password, and check "Is admin" if desired. +3. Submit — the frontend calls `POST /api/admin/users`. +4. Admins cannot delete themselves via `DELETE /api/admin/users/:id`. + +### Managing Set Permissions + +- `GET /api/admin/permissions` — list permissions matrix +- `POST /api/admin/permissions` — grant access to a set (`body: { set_id, user_id, role: "owner" | "viewer" }`) +- `DELETE /api/admin/permissions` — revoke access (`body: { set_id, user_id }`) + +Roles: +- `owner` — can upload to the set, soft-delete / restore media, regenerate thumbnails +- `viewer` — can browse and play media in the set + +Admins implicitly see all sets without explicit permission rows. + +### Rescanning the Library + +Click **Rescan** in the admin panel, or call: + +```bash +curl -X POST -b session=<cookie> http://<host>/api/admin/rescan +``` + +This triggers `FSScanner.Scan()`, which: +1. Scans immediate subdirectories of `MEDIA_ROOT` as **sets** +2. Recursively walks each set for supported media files +3. Probes new files with `ffprobe` +4. Generates thumbnails for video files +5. Inserts new records into the `media` table + +--- + +## Configuration via Environment Variables + +`internal/config.go` loads all settings from the environment. + +| Variable | Default | Validation | Description | +|----------|---------|------------|-------------| +| `PORT` | `8080` | 1–65535 | HTTP listen port | +| `MEDIA_ROOT` | `./media` | — | Root path for media set directories | +| `DB_PATH` | `data.db` | — | SQLite database file path | +| `MAX_UPLOAD_SIZE_MB` | `100` | ≥ 1 | Max upload size per file (MB) | +| `SESSION_TIMEOUT_HOURS` | `24` | ≥ 1 | Cookie / session expiry | +| `GC_INTERVAL_MINUTES` | `30` | ≥ 1 | Garbage collector tick interval | +| `SHARE_DEFAULT_EXPIRY_DAYS` | `7` | ≥ 1 | Default share link lifetime | +| `LOG_LEVEL` | `info` | `debug` / `info` / `warn` / `error` | Log verbosity | + +**Important:** The K8s `Deployment` overrides `DB_PATH` to `/data/media.db` and `MEDIA_ROOT` to `/media` so the PVC mounts are used. Do not rely on the local defaults in a container. + +--- + +## Notes for Agents + +- When modifying tests, always run `go test ./... -race -cover` before committing. +- Do not introduce package-level mutable state; inject via constructors. +- All repository access goes through the `repository.Store` interface. +- Frontend modules are plain ES modules — no transpilation step. Keep JS vanilla. +- CSS changes must use `var(--*)` tokens from `theme.css`. +- If you add new env vars, update both `internal/config.go` and this document. @@ -76,8 +76,8 @@ A self-hosted, Kubernetes-deployable web media player written in Go (stdlib `net │ │ ├── player.css # Custom overlay, fullscreen progress-bar-visible │ │ ├── layout.css # Responsive grid/flex │ │ ├── login.css # Login page layout -│ │ └── themes/ -│ │ └── light.css # Future swap + │ │ └── themes/ # (empty; themes toggled via data-theme in theme.css) + │ └── js/ │ ├── app.js # Router, auth bootstrap, orchestration │ ├── api.js # Fetch wrapper (credentials: include) @@ -330,13 +330,13 @@ CREATE INDEX idx_shares_expires ON shares(expires_at); ### 9. Upload - `POST /api/sets/:id/upload` with `multipart/form-data`. -- Max file size: **500MB** (`MAX_UPLOAD_SIZE_MB=500`). +- Max file size: **100MB** (`MAX_UPLOAD_SIZE_MB=100`). - If filename exists, append `(1)`, `(2)`, etc. - After save, immediate `ffprobe` + `ffmpeg` thumbnail + insert into `media`. ### 10. Share Links - `POST /api/media/:id/shares` generates a new random token. -- Default expiration: **14 days** from now. +- Default expiration: **7 days** from now. - Public routes `/s/:token` and `/s/:token/stream` bypass auth. - Each time `s` is pressed, a **new share** is created (old ones remain valid until expiry). @@ -344,7 +344,7 @@ CREATE INDEX idx_shares_expires ON shares(expires_at); - `DELETE /api/media/:id` sets `deleted_at = NOW()`. - Media hidden from normal views; shown in admin trash view. - Admin/owner can restore before 7 days. -- Background goroutine (`time.Ticker` hourly) selects items where `deleted_at < NOW() - 7 days`. +- Background goroutine (`time.Ticker`, default every 30 minutes) selects items where `deleted_at < NOW() - 7 days`. - Physical file deleted via `os.Remove()`, then **hard DELETE** from DB row. ### 12. Thumbnail Regeneration @@ -396,12 +396,12 @@ CREATE INDEX idx_shares_expires ON shares(expires_at); | Variable | Default | Description | |----------|---------|-------------| | `PORT` | `8080` | HTTP listen port | -| `MEDIA_ROOT` | `/media` | Root path for set directories | -| `DB_PATH` | `/data/media.db` | SQLite database file | -| `MAX_UPLOAD_SIZE_MB` | `500` | Max upload size per file | +| `MEDIA_ROOT` | `./media` | Root path for set directories | +| `DB_PATH` | `data.db` | SQLite database file | +| `MAX_UPLOAD_SIZE_MB` | `100` | Max upload size per file | | `SESSION_TIMEOUT_HOURS` | `24` | Cookie expiry | -| `GC_INTERVAL_MINUTES` | `60` | Garbage collector tick | -| `SHARE_DEFAULT_EXPIRY_DAYS` | `14` | Default share link lifetime | +| `GC_INTERVAL_MINUTES` | `30` | Garbage collector tick | +| `SHARE_DEFAULT_EXPIRY_DAYS` | `7` | Default share link lifetime | | `LOG_LEVEL` | `info` | Log verbosity | --- @@ -440,7 +440,7 @@ All 31 features implemented, tested, and deployable: 17. Theming (CSS variables) 18. Dark Mode Toggle 19. Upload (500MB, owner/admin) -20. Share Links (s key, 14 days) +20. Share Links (s key, 7 days) 21. Shuffle (r key, filtered scope) 22. Keyboard Navigation 23. Selection Highlight |
