Claude: Compact repo rules to reduce context size

This commit is contained in:
Cathie Integra 2026-04-17 08:16:17 +02:00
parent 79b3f569a8
commit 7e6773e94e
9 changed files with 117 additions and 188 deletions

View file

@ -17,7 +17,7 @@ Run `make help` to list all available targets. Key commands:
**Dependencies:**
- `make dep` — install all dependencies (TensorFlow models, ONNX models, JS packages)
- `make dep-js` — install JS dependencies only (`npm ci`). The `photoprism/develop` base image sets the npm env to ignore install scripts; when running `npm ci` or `npm install` outside that image (e.g. in a coding-agent env that doesn't inherit it), pass `--ignore-scripts` explicitly to mitigate supply-chain attacks.
- `make dep-js` — install JS dependencies only (`npm ci`). Prefer `make dep-js` inside the dev container (the `photoprism/develop` base image already ignores install scripts); if you must run npm directly outside it, add `--ignore-scripts` and rebuild only the native addons that need it.
**Docker dev environment:**
- `make docker-build` — build local Docker image

View file

@ -1,22 +1,20 @@
## API & Config Changes
- Respect precedence: `options.yml` overrides CLI/env values, which override defaults.
- When adding a new option: update `internal/config/options.go` (yaml/flag tags), register it in `internal/config/flags.go`, expose a getter, surface it in `*config.Report()`, and write generated values back to `options.yml`. Use `CliTestContext` in `internal/config/test.go` to exercise new flags.
- For `options.yml` writes in Go code, prefer config-owned persistence helpers: use `Config.SaveOptionsPatch(...)` for generic merges and `Config.SaveClusterOptionsUpdate(...)` for cluster-managed metadata updates.
- Use `pkg/fs.ConfigFilePath` when you need a config filename so existing `.yml` files remain valid and new installs can adopt `.yaml` transparently.
- Use the public accessors on `*config.Config` (e.g. `Config.JWKSUrl()`, `Config.SetJWKSUrl()`) instead of mutating `Config.Options()` directly; reserve raw option tweaks for test fixtures only.
- When introducing new metadata sources (e.g., `SrcOllama`, `SrcOpenAI`), define them in both `internal/entity/src.go` and the frontend lookup tables (`frontend/src/common/util.js`).
- Config init order: load `options.yml` (`c.initSettings()`), run `EarlyExt().InitEarly(c)`, connect/register the DB, then invoke `Ext().Init(c)`.
- Adding a new option: update `internal/config/options.go` (yaml/flag tags), register in `internal/config/flags.go`, expose a getter, surface it in `*config.Report()`, and write generated values back to `options.yml`. Use `CliTestContext` in `internal/config/test.go` to exercise new flags.
- For `options.yml` writes, prefer config-owned persistence helpers: `Config.SaveOptionsPatch(...)` for generic merges, `Config.SaveClusterOptionsUpdate(...)` for cluster-managed metadata.
- Use `pkg/fs.ConfigFilePath` for config filenames so existing `.yml` files stay valid and new installs can adopt `.yaml` transparently.
- Use the public accessors on `*config.Config` (e.g. `JWKSUrl()`, `SetJWKSUrl()`) instead of mutating `Config.Options()` directly; reserve raw option tweaks for test fixtures.
- New metadata sources (e.g. `SrcOllama`, `SrcOpenAI`) must be defined in both `internal/entity/src.go` and the frontend lookup tables (`frontend/src/common/util.js`).
- Config init order: load `options.yml` (`c.initSettings()`), run `EarlyExt().InitEarly(c)`, connect/register the DB, then `Ext().Init(c)`.
- Favor explicit CLI flags: check `c.cliCtx.IsSet("<flag>")` before overriding user-supplied values.
- Database helpers: reuse `conf.Db()` / `conf.Database*()`, avoid GORM `WithContext`, quote MySQL identifiers, and reject unsupported drivers early.
## Handler Conventions
- Reuse limiter stacks (`limiter.Auth`, `limiter.Login`) and `limiter.AbortJSON` for 429s.
- Lean on `api.ClientIP`, `header.BearerToken`, and `Abort*` helpers.
- Compare secrets with constant-time checks, set `Cache-Control: no-store` on sensitive responses.
- Register routes in `internal/server/routes.go`.
- For new list endpoints default `count=100` (max 1000) and `offset≥0`, document parameters explicitly.
- Reuse limiter stacks (`limiter.Auth`, `limiter.Login`) and `limiter.AbortJSON` for 429s. Lean on `api.ClientIP`, `header.BearerToken`, and `Abort*` helpers.
- Compare secrets with constant-time checks; set `Cache-Control: no-store` on sensitive responses.
- Register routes in `internal/server/routes.go`. New list endpoints default `count=100` (max 1000) and `offset≥0`; document parameters explicitly.
- Set portal mode via `PHOTOPRISM_NODE_ROLE=portal` plus `PHOTOPRISM_JOIN_TOKEN` when needed.
## API Shape Checklist
@ -29,15 +27,13 @@ When renaming or adding fields:
## Testing Helpers
- Isolate config paths with `t.TempDir()`, reuse `NewConfig`, `CliTestContext`, and `NewApiTest()` harnesses.
- Authenticate via `AuthenticateAdmin`, `AuthenticateUser`, or `OAuthToken`.
- Toggle auth with `conf.SetAuthMode(config.AuthModePasswd)`.
- Isolate config paths with `t.TempDir()`; reuse `NewConfig`, `CliTestContext`, and `NewApiTest()` harnesses.
- Authenticate via `AuthenticateAdmin`, `AuthenticateUser`, or `OAuthToken`. Toggle auth with `conf.SetAuthMode(config.AuthModePasswd)`.
- Prefer OAuth client tokens over non-admin fixtures for negative permission checks.
## Roles & ACL
- Map roles via the shared tables: users through `acl.ParseRole(s)` / `acl.UserRoles[...]`, clients through `acl.ClientRoles[...]`.
- Treat `RoleAliasNone` ("none") and an empty string as `RoleNone`; no caller-specific overrides.
- Default unknown client roles to `RoleClient`.
- Build CLI role help from `Roles.CliUsageString()` (e.g., `acl.ClientRoles.CliUsageString()`); never hand-maintain role lists.
- When checking JWT/client scopes, use the shared helpers (`acl.ScopePermits` / `acl.ScopeAttrPermits`).
- Treat `RoleAliasNone` ("none") and an empty string as `RoleNone`; default unknown client roles to `RoleClient`.
- Build CLI role help from `Roles.CliUsageString()` (e.g. `acl.ClientRoles.CliUsageString()`) — never hand-maintain role lists.
- For JWT/client scope checks, use the shared helpers (`acl.ScopePermits` / `acl.ScopeAttrPermits`).

View file

@ -1,40 +1,35 @@
## Cluster Operations
- Keep bootstrap code decoupled: avoid importing `internal/service/cluster/node/*` from `internal/config` or the cluster root, let nodes talk to the Portal over HTTP(S), and rely on constants from `internal/service/cluster/const.go`.
- Bootstrap refreshes node OAuth credentials on 401/403 responses (rotate secret + retry) and logs the refresh at info level. If the secret file cannot be written, the rotated value stays cached in memory so the current process can continue.
- Portal validation now accepts HTTP advertise URLs only for loopback hosts or cluster-internal domains (`*.svc`, `*.cluster.local`, `*.internal`); everything else must use HTTPS.
- Theme endpoint: `GET /api/v1/cluster/theme` streams a zip from `conf.ThemePath()`; only reinstall when `app.js` is missing and always use the header helpers in `pkg/http/header`.
- Registration flow: send `rotate=true` only for MySQL/MariaDB nodes without credentials, treat 401/403/404 as terminal, include `ClientID` + `ClientSecret` when renaming an existing node, and persist only newly generated secrets or DB settings.
- Keep bootstrap decoupled: avoid importing `internal/service/cluster/node/*` from `internal/config` or the cluster root; nodes talk to the Portal over HTTP(S) using constants from `internal/service/cluster/const.go`.
- Bootstrap refreshes node OAuth credentials on 401/403 (rotate secret + retry, info-level log). If the secret file can't be written, the rotated value stays cached in memory so the current process continues.
- Portal validation accepts HTTP advertise URLs only for loopback or cluster-internal domains (`*.svc`, `*.cluster.local`, `*.internal`); everything else must use HTTPS.
- Theme endpoint: `GET /api/v1/cluster/theme` streams a zip from `conf.ThemePath()`; reinstall only when `app.js` is missing and always use the header helpers in `pkg/http/header`.
- Registration flow: send `rotate=true` only for MySQL/MariaDB nodes without credentials; treat 401/403/404 as terminal; include `ClientID` + `ClientSecret` when renaming an existing node; persist only newly generated secrets or DB settings.
### Registry & DTOs
- Use the client-backed registry (`NewClientRegistryWithConfig`)—the file-backed version is legacy.
- Nodes are keyed by UUID v7 (`/api/v1/cluster/nodes/{uuid}`), the registry interface stays UUID-first (`Get`, `FindByNodeUUID`, `FindByClientID`, `RotateSecret`, `DeleteAllByUUID`).
- CLI lookups resolve `uuid → ClientID → name`.
- DTOs normalize `Database.{Name,User,Driver,RotatedAt}` while exposing `ClientSecret` only during creation/rotation.
- `nodes rm --all-ids` cleans duplicate client rows.
- Use the client-backed registry (`NewClientRegistryWithConfig`) — the file-backed version is legacy.
- Nodes are keyed by UUID v7 (`/api/v1/cluster/nodes/{uuid}`); registry interface is UUID-first (`Get`, `FindByNodeUUID`, `FindByClientID`, `RotateSecret`, `DeleteAllByUUID`). CLI lookups resolve `uuid → ClientID → name`.
- DTOs normalize `Database.{Name,User,Driver,RotatedAt}`; `ClientSecret` is exposed only during creation/rotation. `nodes rm --all-ids` cleans duplicate client rows. `ClientData` no longer stores `NodeUUID`.
- Admin responses may include `AdvertiseUrl`/`Database`; client/user sessions stay redacted.
- Registry files live under `conf.PortalConfigPath()/nodes/` (mode 0600).
- `ClientData` no longer stores `NodeUUID`.
### Provisioner & DSN
- Database/user names use UUID-based HMACs (`<prefix>d<hmac11>`, `<prefix>u<hmac11>` where the prefix defaults to `cluster_`).
- `BuildDSN` accepts a `driver` but falls back to MySQL format with a warning when unsupported.
- If Postgres provisioning is added, extend `BuildDSN` and `provisioner.DatabaseDriver` handling, add validations, and return `driver=postgres` consistently in API and CLI output.
- Database/user names use UUID-based HMACs (`<prefix>d<hmac11>`, `<prefix>u<hmac11>`; prefix defaults to `cluster_`).
- `BuildDSN` accepts a `driver` but falls back to MySQL format with a warning when unsupported. For Postgres, extend `BuildDSN` and `provisioner.DatabaseDriver` handling, add validations, and return `driver=postgres` consistently in API and CLI output.
### Sessions & Redaction
- Admin session (full view): `AuthenticateAdmin(app, router)`.
- User session: Create a non-admin test user (role=guest), set a password, then `AuthenticateUser`.
- User session: create a non-admin test user (role=guest), set a password, then `AuthenticateUser`.
- Client session (redacted internal fields; `SiteUrl` visible):
```go
s, _ := entity.AddClientSession("test-client", conf.SessionMaxAge(), "cluster", authn.GrantClientCredentials, nil)
token := s.AuthToken()
r := AuthenticatedRequest(app, http.MethodGet, "/api/v1/cluster/nodes", token)
```
- Admins see `AdvertiseUrl` and `Database`; client/user sessions don't. `SiteUrl` is safe to show to all roles.
- Client config includes `storageNamespace` (SHA-256 of `SiteUrl`) for browser storage scoping.
- Admins see `AdvertiseUrl` and `Database`; client/user sessions don't. `SiteUrl` is safe for all roles. Client config includes `storageNamespace` (SHA-256 of `SiteUrl`) for browser storage scoping.
### Preflight Checklist
@ -43,4 +38,4 @@
- `go test ./internal/service/cluster/registry -count=1`
- `go test ./internal/api -run 'Cluster' -count=1`
- `go test ./internal/commands -run 'ClusterRegister|ClusterNodesRotate' -count=1`
- Tooling constraints: `make swag` may fetch modules, so confirm network access before running it.
- Tooling: `make swag` may fetch modules — confirm network access before running.

View file

@ -1,15 +1,20 @@
## Frontend Test Coverage
- New JavaScript functions, including helpers, should be tested whenever practical; update existing tests or add new ones as needed.
- New Vue components should have component-test coverage, and existing component tests should be updated as needed when component behavior changes.
- Test new JS functions (including helpers) and new Vue components whenever practical; update existing tests when behavior changes.
## Frontend Linting & Test Entry Points
- Use the lint and format scripts declared in `frontend/package.json`; all added JS, Vue, and frontend tests must follow those standards.
- Frontend unit tests use Vitest. Common entry points: `make test-js`, `make vitest-watch`, `make vitest-coverage`.
- Acceptance tests use the `acceptance-*` targets in the root `Makefile`.
- For one-off TestCafe checks, keep startup and cleanup in the repository root: `make storage/acceptance`, `make acceptance-sqlite-restart`, `make wait-2`, then run the testcafe command from `frontend/`, then `make acceptance-sqlite-stop`.
- If a command temporarily changes into `frontend/`, return to the repository root before running `make acceptance-sqlite-stop`.
- Follow the lint/format scripts in `frontend/package.json`; all added JS, Vue, and tests must conform.
- Unit tests (Vitest): `make test-js`, `make vitest-watch`, `make vitest-coverage`. Acceptance: `acceptance-*` targets in the root `Makefile`.
- One-off TestCafe (single case by `testID`):
```bash
make storage/acceptance
make acceptance-sqlite-restart
make wait-2
(cd frontend && npm run testcafe -- "chrome --headless=new --use-gl=angle --use-angle=swiftshader --disable-features=LocalNetworkAccessChecks" --config-file ./testcaferc.json --test-meta mode=public,type=short,testID=components-001 "tests/acceptance")
make acceptance-sqlite-stop
```
Always return to repo root before `make acceptance-sqlite-stop`.
## Frontend Test Gotchas
@ -17,34 +22,29 @@
## Playwright MCP Usage
- Default endpoint is `http://localhost:2342/`; default login routes are `/library/login` for CE, Plus, and Pro, and `/portal/admin/login` for Portal.
- Use the local compose admin credentials; if login fails, inspect the active compose environment.
- Desktop sessions default to `1280x900`; mobile sessions should use the mobile Playwright server with `375x667`.
- Close the browser tab after scripted interactions.
- Prefer waits over sleeps, click only visible and enabled elements, and use role, label, or text selectors instead of brittle XPath selectors.
- Keep screenshots small and reproducible: prefer JPEG, visible viewport, deterministic `.local/screenshots/<case>/<step>__<viewport>.jpg` names, and no large inline screenshots.
- If `npx` fetches an MCP server at runtime, add `--yes` or preinstall it to avoid prompts.
- Endpoint `http://localhost:2342/`; logins at `/library/login` (CE/Plus/Pro) and `/portal/admin/login` (Portal). Use local compose admin credentials; if login fails, inspect the active compose env.
- Viewports: desktop `1280x900`; mobile uses the mobile Playwright server at `375x667`. Close the browser tab after scripted interactions.
- Prefer waits over sleeps; click only visible/enabled elements; use role/label/text selectors (not XPath).
- Screenshots: small and reproducible — JPEG, visible viewport, deterministic `.local/screenshots/<case>/<step>__<viewport>.jpg` names, no large inline screenshots.
- If `npx` fetches an MCP server at runtime, add `--yes` or preinstall to avoid prompts.
## Frontend Focus Management
- Dialogs must follow the shared focus pattern documented in `frontend/src/common/README.md`.
- Always expose `ref="dialog"` on `<v-dialog>` overlays, call `$view.enter/leave` in `@after-enter` / `@after-leave`, and avoid positive `tabindex` values.
- Persistent dialogs (those with the `persistent` prop) must handle Escape via `@keydown.esc.exact` so Vuetify's default rejection animation is suppressed; keep other shortcuts on `@keyup` so inner inputs can cancel them first.
- Global shortcuts run through `onShortCut(ev)` in `common/view.js`; it only forwards Escape and `ctrl`/`meta` combinations, so do not rely on it for arbitrary keys.
- When a dialog opens nested menus (e.g., combobox suggestion lists), ensure they work with the global trap; see the README for troubleshooting tips.
- Dialogs must follow the shared pattern in `frontend/src/common/README.md`: expose `ref="dialog"` on `<v-dialog>`, call `$view.enter/leave` in `@after-enter` / `@after-leave`, and avoid positive `tabindex`.
- Persistent dialogs (`persistent` prop) must handle Escape via `@keydown.esc.exact` to suppress Vuetify's rejection animation; keep other shortcuts on `@keyup` so inner inputs can cancel first.
- Global shortcuts go through `onShortCut(ev)` in `common/view.js`, which only forwards Escape and `ctrl`/`meta` combos — don't rely on it for arbitrary keys.
- When a dialog opens nested menus (e.g., combobox suggestions), confirm they work with the global trap; see the README for troubleshooting.
## Frontend Translations
- Frontend translation extraction source of truth is root `make gettext-extract` (runs `scripts/gettext-extract.sh`), which scans `frontend/src` plus available private overlays in `plus/frontend`, `pro/frontend`, and `portal/frontend`.
- Avoid punctuation-only gettext keys (e.g. `$gettext("—")`), as they create noisy/unhelpful entries in `frontend/src/locales/translations.pot`.
- Extraction source of truth: root `make gettext-extract` (via `scripts/gettext-extract.sh`), which scans `frontend/src` plus available overlays in `plus/frontend`, `pro/frontend`, `portal/frontend`.
- Avoid punctuation-only gettext keys (e.g. `$gettext("—")`) — they clutter `frontend/src/locales/translations.pot`.
## Web Templates & Shared Assets
- HTML entrypoints live under `assets/templates/`; key files are `index.gohtml`, `app.gohtml`, `app.js.gohtml`, and `splash.gohtml`.
- Browser check logic: `assets/static/js/browser-check.js` is included via `app.js.gohtml`; it performs capability checks before the main bundle executes.
- OIDC login completion for the web UI is bridged through `assets/templates/auth.gohtml`, which writes the session into namespaced browser storage. Must stay aligned with `frontend/src/common/session.js`, `frontend/src/common/storage.js`, and the login form toggle in `frontend/src/page/auth/login.vue`.
- When touching frontend session bootstrap, verify that `frontend/src/common/session.js` resolves `storageNamespace` from the real client config shape (`window.__CONFIG__` / `config.values`), not only from simplified mocks. Include a focused test that would fail if session restore fell back to the `pp:root:` namespace.
- Keep the script order in `app.js.gohtml` so `browser-check.js` loads before the bundle script. Do not add `defer` or `async` to the bundle tag unless you reintroduce a guarded loader.
- The same loader partial is reused in private packages (`pro/assets/templates/index.gohtml`, `plus/assets/templates/index.gohtml`, `portal/assets/templates/index.gohtml`). Whenever you change `app.js.gohtml` or bundle loading, verify those files still include the shared partial.
- Splash styles: `frontend/src/css/splash.css`. Add new splash elements there for visual consistency across editions.
- Browser baseline: Safari 13 / iOS 13 or current Chrome, Edge, or Firefox.
- HTML entrypoints live in `assets/templates/`: `index.gohtml`, `app.gohtml`, `app.js.gohtml`, `splash.gohtml`. `assets/static/js/browser-check.js` runs capability checks before the main bundle; keep it loaded before the bundle script in `app.js.gohtml` and don't add `defer`/`async` to the bundle tag unless you reintroduce a guarded loader.
- OIDC login completion bridges through `assets/templates/auth.gohtml`, writing the session into namespaced browser storage — must stay aligned with `frontend/src/common/session.js`, `frontend/src/common/storage.js`, and the login-form toggle in `frontend/src/page/auth/login.vue`.
- When touching session bootstrap, verify `session.js` resolves `storageNamespace` from the real client-config shape (`window.__CONFIG__` / `config.values`), not just mocks. Add a focused test that would fail if restore fell back to `pp:root:`.
- The loader partial is reused in `pro/`, `plus/`, and `portal/assets/templates/index.gohtml`; verify they still include it whenever `app.js.gohtml` or bundle loading changes.
- Splash styles: `frontend/src/css/splash.css` — add new splash elements there for cross-edition consistency.
- Browser baseline: Safari 13 / iOS 13 or current Chrome, Edge, Firefox.

View file

@ -1,20 +1,18 @@
## Go Code Style
- Run `make lint-go` (golangci-lint) after Go changes; prefer `golangci-lint run ./internal/<pkg>/...` for focused edits.
- Doc comments for packages and exported identifiers must be complete sentences that begin with the name of the thing being described and end with a period.
- All newly added functions, including unexported helpers, must have a concise doc comment that explains their behavior.
- Doc comments for packages and exported identifiers must be complete sentences that begin with the name of the thing being described and end with a period. All new functions (including unexported helpers) need a concise doc comment explaining behavior.
- For short examples inside comments, indent code rather than using backticks; godoc treats indented blocks as preformatted.
- Every Go package must contain a `<package>.go` file in its root (for example, `internal/auth/jwt/jwt.go`) with the standard license header and a short package description comment explaining its purpose.
- Go is formatted by `gofmt` and uses tabs. Do not hand-format indentation. Always run after edits: `make fmt-go` (gofmt + goimports).
- Every Go package must contain a `<package>.go` file in its root (e.g. `internal/auth/jwt/jwt.go`) with the standard license header and a short package description comment.
- Go is formatted by `gofmt` with tabs. Do not hand-format indentation. After edits run `make fmt-go` (gofmt + goimports).
## Package Boundaries
- Code in `pkg/*` MUST NOT import from `internal/*`.
- If you need access to config/entity/DB, put new code in a package under `internal/` instead of `pkg/`.
- Code in `pkg/*` MUST NOT import from `internal/*`. If you need config/entity/DB access, put new code under `internal/` instead.
## GORM Field Naming
When adding struct fields that include uppercase abbreviations (e.g., `LabelNSFW`, `UserID`, `URLHash`), set an explicit `gorm:"column:<name>"` tag so column names stay consistent (`label_nsfw`, `user_id`, `url_hash` instead of split-letter variants).
When adding struct fields with uppercase abbreviations (e.g. `LabelNSFW`, `UserID`, `URLHash`), set an explicit `gorm:"column:<name>"` tag so column names stay consistent (`label_nsfw`, `user_id`, `url_hash` instead of split-letter variants).
## Filesystem Permissions & io/fs Aliasing
@ -24,14 +22,11 @@ When adding struct fields that include uppercase abbreviations (e.g., `LabelNSFW
- Config files: `fs.ModeConfigFile` (default 0o664)
- Secrets/tokens: `fs.ModeSecretFile` (default 0o600)
- Backups: `fs.ModeBackupFile` (default 0o600)
- Do not pass stdlib `io/fs` flags (e.g., `fs.ModeDir`) to functions expecting permission bits.
- When importing the stdlib package, alias it to avoid collisions: `iofs "io/fs"` or `gofs "io/fs"`.
- Prefer `filepath.Join` for filesystem paths; reserve `path.Join` for URL paths.
- For slash-based logical paths stored in DB/config/API payloads (e.g. folder album paths), normalize with `clean.SlashPath(...)` instead of repeating ad-hoc `strings.ReplaceAll(..., "\\", "/")` + trim logic.
- Do not pass stdlib `io/fs` flags to functions expecting permission bits. When importing the stdlib package, alias it to avoid collisions: `iofs "io/fs"` or `gofs "io/fs"`.
- Prefer `filepath.Join` for filesystem paths; reserve `path.Join` for URL paths. For slash-based logical paths stored in DB/config/API payloads (e.g. folder album paths), normalize with `clean.SlashPath(...)` instead of ad-hoc `strings.ReplaceAll(..., "\\", "/")` + trim logic.
## Logging
- Use the shared logger (`event.Log`) via the package-level `log` variable (see `internal/auth/jwt/logger.go`) instead of direct `fmt.Print*` or ad-hoc loggers.
- Logging terminology: in human-readable log text, prefer canonical runtime terms (`instance`, `service`) and reserve `node` for contract-bound names (`/cluster/nodes`, `Node*`, `PHOTOPRISM_NODE_*`).
- Audit outcomes: import `github.com/photoprism/photoprism/pkg/log/status` and end every `event.Audit*` slice with a single outcome token such as `status.Succeeded`, `status.Failed`, `status.Denied`.
- Error outcomes: when a sanitized error string should be the outcome, call `status.Error(err)` instead of adding a placeholder and passing `clean.Error(err)` manually.
- Terminology: in human-readable log text, prefer canonical runtime terms (`instance`, `service`) and reserve `node` for contract-bound names (`/cluster/nodes`, `Node*`, `PHOTOPRISM_NODE_*`).
- Audit outcomes: import `github.com/photoprism/photoprism/pkg/log/status` and end every `event.Audit*` slice with a single outcome token such as `status.Succeeded`, `status.Failed`, `status.Denied`. When a sanitized error string should be the outcome, call `status.Error(err)` instead of manually passing `clean.Error(err)`.

View file

@ -1,59 +1,49 @@
## Go Test Coverage
- Every new Go function, including unexported helpers, must have focused test coverage in the corresponding `*_test.go` file; update existing tests or add new ones as needed.
- Refactors count too: when you split an existing function into new helpers (e.g., splitting one route registrar into two), each new function needs its own `Test<Name>` with at least a Success and an InvalidRequest/error case. Do not rely on the old test name still covering the old path by side effect.
- Before reporting a Go change as done, grep your diff for `^func ` additions and confirm each has a matching `Test*` entry. If you cannot justify skipping coverage for a helper, add the test.
- Regenerating `swagger.json` or updating route registration is not a substitute for tests — Swagger documents shape, tests prove behavior. Both are required.
- Every new Go function (including unexported helpers) must have focused coverage in a sibling `*_test.go`. Refactors count: each new helper needs its own `Test<Name>` with at least a Success and an error/InvalidRequest case — don't rely on the old test covering the new path.
- Before reporting a change done, grep your diff for `^func ` additions and confirm each has a matching `Test*`. Swagger or route regeneration is not a substitute — Swagger documents shape, tests prove behavior.
## Go Testing Patterns
- Go tests live next to their sources (`path/to/pkg/<file>_test.go`); group related cases as `t.Run(...)` sub-tests to keep table-driven coverage readable. Use **PascalCase** for subtest names (e.g., `t.Run("Success", ...)`).
- Do not run multiple test commands in parallel. Suites share fixture files, temporary assets, and database state, so concurrent runs can trigger false failures or fixture conflicts.
- Keep Go scratch work inside `internal/...`; Go refuses to import `internal/` packages from directories like `/tmp`.
- Prefer focused `go test` runs for speed (`go test ./internal/<pkg> -run <Name> -count=1`) and avoid `./...` unless you need the entire suite.
- Heavy packages such as `internal/entity` and `internal/photoprism` run migrations and fixtures; expect 30120s on first run and narrow with `-run` to keep iterations low.
- Tests live next to sources (`<file>_test.go`); group cases with `t.Run(...)` using **PascalCase** names (`Success`, `InvalidRequest`).
- Do not run multiple test commands in parallel — suites share fixtures, temp assets, and DB state.
- Keep Go scratch work inside `internal/...` (Go refuses `internal/` imports from `/tmp`).
- Prefer focused runs: `go test ./internal/<pkg> -run <Name> -count=1`. Avoid `./...` unless needed; heavy packages (`internal/entity`, `internal/photoprism`) take 30120s on first run.
### Fast, Focused Test Recipes
- Filesystem + archives (fast): `go test ./pkg/fs -run 'Copy|Move|Unzip' -count=1`
- FS + archives (fast): `go test ./pkg/fs -run 'Copy|Move|Unzip' -count=1`
- Media helpers (fast): `go test ./pkg/media/... -count=1`
- Thumbnails (libvips, moderate): `go test ./internal/thumb/... -count=1`
- FFmpeg command builders (moderate): `go test ./internal/ffmpeg -run 'Remux|Transcode|Extract' -count=1`
- FFmpeg builders (moderate): `go test ./internal/ffmpeg -run 'Remux|Transcode|Extract' -count=1`
### Test Config Helpers
- Use `config.TestConfig()` for shared fixtures (runs `InitializeTestData()`, wipes `storage/testdata`).
- Prefer `config.NewMinimalTestConfig(t.TempDir())` when a test only needs filesystem/config scaffolding.
- Use `config.NewMinimalTestConfigWithDb("<name>", t.TempDir())` for a fresh SQLite schema without the cached fixture snapshot.
- Config test helpers auto-discover the repo `assets/` directory; do not set `PHOTOPRISM_ASSETS_PATH` manually in package `init()` functions.
- Hub API traffic is disabled in tests by default via `hub.ApplyTestConfig()`; opt back in with `PHOTOPRISM_TEST_HUB=test`.
- Avoid `config.TestConfig()` in new tests unless you truly need the fully seeded fixture set.
- Default to `config.NewMinimalTestConfig(t.TempDir())` for FS/config scaffolding, or `config.NewMinimalTestConfigWithDb("<name>", t.TempDir())` for a fresh SQLite schema.
- Reserve `config.TestConfig()` for tests that truly need the fully seeded fixture snapshot (runs `InitializeTestData()`, wipes `storage/testdata`).
- Config helpers auto-discover `assets/`; don't set `PHOTOPRISM_ASSETS_PATH` in `init()`. Hub traffic is disabled by default; re-enable with `PHOTOPRISM_TEST_HUB=test`.
### Fixtures
- Shared fixtures live under `storage/testdata`; `NewTestConfig("<pkg>")` calls `InitializeTestData()`, but call `c.InitializeTestData()` (and optionally `c.AssertTestData(t)`) when you construct custom configs.
- `PhotoFixtures.Get()` and similar helpers return value copies; when a test needs the database-backed row, re-query by UID/ID using helpers like `entity.FindPhoto(fixture)`.
- When adding persistent fixtures, always obtain new IDs via `rnd.GenerateUID(...)` with the matching prefix (`entity.PhotoUID`, `entity.FileUID`, `entity.LabelUID`, …).
- For database updates, prefer the `entity.Values` type alias over raw `map[string]interface{}`.
- Generate identifiers with `rnd.GenerateUID(entity.ClientUID)` for OAuth client IDs and `rnd.UUIDv7()` for node UUIDs; treat `node.uuid` as required in responses.
- When you need illustrative credentials (join tokens, client IDs/secrets, etc.), reuse the shared `Example*` constants (see `internal/service/cluster/examples.go`).
- `NewTestConfig("<pkg>")` runs `InitializeTestData()`; for custom configs call `c.InitializeTestData()` (and optionally `c.AssertTestData(t)`).
- `PhotoFixtures.Get()` etc. return value copies — re-query via `entity.FindPhoto(fixture)` when you need the DB row.
- New persistent IDs: `rnd.GenerateUID(entity.PhotoUID|FileUID|LabelUID|ClientUID|…)`; node UUIDs use `rnd.UUIDv7()` and `node.uuid` is required in responses.
- Use `entity.Values` (not raw `map[string]interface{}`) for DB updates. Reuse shared `Example*` constants for illustrative credentials (see `internal/service/cluster/examples.go`).
### CLI Testing Gotchas
- `urfave/cli` calls `os.Exit(code)` when a command returns `cli.Exit(...)`; use the test helper `RunWithTestContext` (in `internal/commands/commands_test.go`) which temporarily overrides `cli.OsExiter`.
- If you only need to assert the exit code, invoke `cmd.Action(ctx)` directly and check `err.(cli.ExitCoder).ExitCode()`.
- Non-interactive mode: set `PHOTOPRISM_CLI=noninteractive` and/or pass `--yes` to avoid prompts that block tests and CI.
- SQLite DSN: `config.NewTestConfig("<pkg>")` defaults to SQLite with a per-suite DSN like `.<pkg>.db`. Don't assert an empty DSN for SQLite.
- Prefer the shared helpers like `DryRunFlag(...)` and `YesFlag()` when adding new CLI flags.
- `urfave/cli` calls `os.Exit` on `cli.Exit(...)`; use `RunWithTestContext` (in `internal/commands/commands_test.go`) or invoke `cmd.Action(ctx)` directly and check `err.(cli.ExitCoder).ExitCode()`.
- Non-interactive: set `PHOTOPRISM_CLI=noninteractive` and/or pass `--yes`.
- SQLite DSN from `NewTestConfig("<pkg>")` is a per-suite path like `.<pkg>.db` — don't assert empty.
- Reuse shared flag helpers (`DryRunFlag(...)`, `YesFlag()`) for new CLI flags.
### FFmpeg Tests & Hardware Gating
### FFmpeg & Hardware Gating
- By default, do not run GPU/HW encoder integrations in CI. Gate with `PHOTOPRISM_FFMPEG_ENCODER`.
- Negative-path tests should remain fast and always run (missing ffmpeg binary → exec error, unwritable destination → fails without creating files).
- Prefer command-string assertions when hardware is unavailable.
- Gate GPU/HW encoder integrations with `PHOTOPRISM_FFMPEG_ENCODER`; CI skips them by default.
- Negative paths (missing ffmpeg, unwritable dest) must stay fast and always run. Prefer command-string assertions when hardware is unavailable.
### API/CLI Test Pitfalls
- Gin routes: Register `CreateSession(router)` once per test router; reusing it twice panics on duplicate route.
- Some commands defer `conf.Shutdown()` or emit signals that close the DB. Avoid invoking `start` or emitting signals in unit tests.
- MariaDB inspection while iterating: connect with `mariadb -D photoprism` and run SQL without rebuilding Go code.
- Register `CreateSession(router)` once per test router — duplicates panic.
- Don't invoke `start` or emit signals in unit tests; some commands defer `conf.Shutdown()` and close the DB.
- MariaDB iteration: `mariadb -D photoprism` for ad-hoc SQL without rebuilding Go.

View file

@ -1,44 +1,26 @@
## Import/Index
- ImportWorker may skip files if an identical file already exists (duplicate detection). Use unique copies or assert DB rows after ensuring a non-duplicate destination.
- Mixed roots: when testing related files, keep `SamplesPath()/ImportPath()/OriginalsPath()` consistent so `RelatedFiles` and `AllowExt` behave as expected.
- `IndexOptions*` helpers now require a `*config.Config`; pass the active config (or `config.NewMinimalTestConfig(t.TempDir())` in unit tests) so face/label/NSFW scheduling matches the current run.
- ImportWorker may skip files if an identical one already exists (duplicate detection). Use unique copies or assert DB rows after ensuring a non-duplicate destination.
- Mixed roots: keep `SamplesPath()/ImportPath()/OriginalsPath()` consistent so `RelatedFiles` and `AllowExt` behave as expected.
- `IndexOptions*` helpers require a `*config.Config`; pass the active config (or `config.NewMinimalTestConfig(t.TempDir())` in unit tests) so face/label/NSFW scheduling matches the current run.
- Folder albums use path-first lookup/update (`album_path`) to avoid slug collisions for emoji child paths.
- Label and label-search logic should reuse `entity.FindLabels(...)`, `entity.FindLabelIDs(...)`, and `entity.LabelSlugs(...)` so homophone-aware exact-name matching stays aligned; avoid adding ad-hoc slug SQL in search code.
- Vision worker scheduling is controlled via `VisionSchedule` / `VisionFilter` and the `Run` property set in `vision.yml`. Utilities like `vision.FilterModels` and `entity.Photo.ShouldGenerateLabels/Caption` help decide when work is required before loading media files.
- Label/label-search logic should reuse `entity.FindLabels(...)`, `entity.FindLabelIDs(...)`, and `entity.LabelSlugs(...)` for homophone-aware exact-name matching — avoid ad-hoc slug SQL in search code.
- Vision worker scheduling is controlled via `VisionSchedule` / `VisionFilter` and `Run` in `vision.yml`. Use `vision.FilterModels` and `entity.Photo.ShouldGenerateLabels/Caption` to decide when work is required before loading media files.
## Download CLI Workbench (yt-dlp, remux, importer)
### Code Anchors
- CLI flags and examples: `internal/commands/download.go`
- Core implementation (testable): `internal/commands/download_impl.go`
- yt-dlp helpers and arg wiring: `internal/photoprism/dl/*` (`options.go`, `info.go`, `file.go`, `meta.go`)
- Importer entry point: `internal/photoprism/get/import.go`; options: `internal/photoprism/import_options.go`
### Quick Test Runs
**Code anchors:** CLI flags/examples in `internal/commands/download.go`; core impl in `internal/commands/download_impl.go`; yt-dlp helpers in `internal/photoprism/dl/*` (`options.go`, `info.go`, `file.go`, `meta.go`); importer entry in `internal/photoprism/get/import.go`; import options in `internal/photoprism/import_options.go`.
**Fast test runs:**
- yt-dlp package: `go test ./internal/photoprism/dl -run 'Options|Created|PostprocessorArgs' -count=1`
- CLI command: `go test ./internal/commands -run 'DownloadImpl|HelpFlags' -count=1`
### FFmpeg-less Tests
**FFmpeg-less tests:** set `c.Options().FFmpegBin = "/bin/false"` and `c.Settings().Index.Convert = false` to avoid ffmpeg dependencies when not validating remux.
Set `c.Options().FFmpegBin = "/bin/false"` and `c.Settings().Index.Convert = false` to avoid ffmpeg dependencies when not validating remux.
### Stubbing yt-dlp (no network)
Use a tiny shell script that:
- prints minimal JSON for `--dump-single-json`
- creates a file and prints its path when `--print` is requested
Harness env vars (supported by our tests):
- `YTDLP_ARGS_LOG` — append final args for assertion
- `YTDLP_OUTPUT_FILE` — absolute file path to create for `--print`
- `YTDLP_DUMMY_CONTENT` — file contents to avoid importer duplicate detection between tests
### Remux Policy
**Stubbing yt-dlp (no network):** use a shell script that prints minimal JSON for `--dump-single-json` and creates a file + prints its path when `--print` is requested. Harness env vars: `YTDLP_ARGS_LOG` (append final args for assertion), `YTDLP_OUTPUT_FILE` (absolute file path to create for `--print`), `YTDLP_DUMMY_CONTENT` (file contents to avoid importer duplicate detection between tests).
**Remux policy:**
- Pipe method: PhotoPrism remux (ffmpeg) always embeds title/description/created.
- File method: yt-dlp writes files; we pass `--postprocessor-args 'ffmpeg:-metadata creation_time=<RFC3339>'` so imports get `Created` even without local remux.
- Default remux policy: `auto`; use `always` for the most complete metadata.
- CLI defaults: `photoprism dl` now defaults to `--method pipe` and `--impersonate firefox`; pass `-i none` to disable impersonation.
- Default policy `auto`; use `always` for the most complete metadata.
- CLI defaults: `photoprism dl` defaults to `--method pipe` and `--impersonate firefox`; pass `-i none` to disable impersonation.

View file

@ -1,46 +1,32 @@
## Safety & Data
- If `git status` shows unexpected changes, assume a human might be editing; ask for permission before using reset commands like `git checkout` or `git reset`.
- If `git status` shows unexpected changes, assume a human might be editing; ask before using reset commands like `git checkout` or `git reset`.
- Do not run `git config` (global or repo-level); changing Git configuration is prohibited for agents.
- Do not run destructive commands against production data. Prefer ephemeral volumes and test fixtures for acceptance tests.
- Never commit secrets, local configurations, or cache files. Use environment variables or a local `.env`.
- Ensure `.env`, `.config`, `.local`, `.codex`, and `.gocache` are ignored in `.gitignore` and `.dockerignore`.
- Prefer using existing caches, workers, and batching strategies referenced in code and `Makefile`.
- Consider memory/CPU impact of changes; only suggest benchmarks or profiling when justified.
- Regenerate `NOTICE` files with `make notice` when dependencies change (e.g., updates to `go.mod`, `go.sum`, `package-lock.json`). Do not edit `NOTICE` or `frontend/NOTICE` manually.
- Never commit secrets, local configurations, or cache files. Use environment variables or a local `.env`. Ensure `.env`, `.config`, `.local`, `.codex`, and `.gocache` are in `.gitignore` and `.dockerignore`.
- Prefer existing caches, workers, and batching strategies in code and `Makefile`. Consider memory/CPU impact of changes; only suggest benchmarks or profiling when justified.
- Regenerate `NOTICE` files with `make notice` when dependencies change (e.g. `go.mod`, `go.sum`, `package-lock.json`). Do not edit `NOTICE` or `frontend/NOTICE` manually.
> If anything in this file conflicts with the `Makefile` or Sources of Truth, **ask** for clarification before proceeding.
## File I/O — Overwrite Policy (force semantics)
- Default is safety-first: callers must not overwrite non-empty destination files unless they opt-in with a `force` flag.
- Replacing empty destination files is allowed without `force=true` (useful for placeholder files).
- Open destinations with `O_WRONLY|O_CREATE|O_TRUNC` to avoid trailing bytes when overwriting; use `O_EXCL` when the caller must detect collisions.
- Default is safety-first: callers must not overwrite non-empty destination files unless they opt-in with `force=true`. Replacing empty destinations is allowed without `force`.
- Open destinations with `O_WRONLY|O_CREATE|O_TRUNC` to avoid trailing bytes when overwriting; use `O_EXCL` when callers must detect collisions.
- Where this lives: `internal/photoprism/mediafile.go` (`MediaFile.Copy/Move`), `pkg/fs/copy.go`, `pkg/fs/move.go`.
- When to set `force=true`: Explicit "replace" actions or admin tools where the user confirmed overwrite. Not for import/index flows; Originals must not be clobbered.
- When to set `force=true`: explicit "replace" actions or admin tools where the user confirmed overwrite. Not for import/index flows Originals must not be clobbered.
## Archive Extraction — Security Checklist
- Always validate ZIP entry names with a safe join; reject:
- absolute paths (e.g., `/etc/passwd`).
- Windows drive/volume paths (e.g., `C:\\…` or `C:/…`).
- any entry that escapes the target directory after cleaning (path traversal via `..`).
- ZIP entry names use slash semantics, not host OS semantics:
- Validate in ZIP-name space with `path.Clean` / `path.IsAbs`, reject backslashes (`\`), and use `path.Base` for hidden-name checks.
- Convert to OS paths only at write time with `filepath.FromSlash(...)`.
- Enforce destination containment with `filepath.Rel(...)` rather than string-prefix checks.
- Enforce per-file and total size budgets to prevent resource exhaustion.
- Skip OS metadata directories (e.g., `__MACOSX`) and reject suspicious names.
- Validate ZIP entry names with a safe join; reject absolute paths (e.g. `/etc/passwd`), Windows drive/volume paths (`C:\\…` or `C:/…`), and any entry that escapes the target directory after cleaning (`..` traversal).
- ZIP entry names use slash semantics, not host OS semantics: validate in ZIP-name space with `path.Clean` / `path.IsAbs`, reject backslashes (`\`), and use `path.Base` for hidden-name checks. Convert to OS paths only at write time via `filepath.FromSlash(...)`. Enforce destination containment with `filepath.Rel(...)` — not string-prefix checks.
- Enforce per-file and total size budgets to prevent resource exhaustion. Skip OS metadata directories (e.g. `__MACOSX`) and reject suspicious names.
- Where this lives: `pkg/fs/zip.go` (`Unzip`, `UnzipFile`, `safeJoin`).
## HTTP Download — Security Checklist
- Use the shared safe HTTP helper: `pkg/http/safe``safe.Download(destPath, url, *safe.Options)`.
- Default policy: allow only `http/https`, enforce timeouts and max size, write to a `0600` temp file then rename.
- SSRF protection (mandatory unless explicitly needed for tests):
- Set `AllowPrivate=false` to block private/loopback/multicast/link-local ranges.
- All redirect targets are validated; the final connected peer IP is also checked.
- Prefer an image-focused `Accept` header for image downloads.
- Avatars and small images: use `internal/thumb/avatar.SafeDownload` (15s timeout, 10 MiB, `AllowPrivate=false`).
- Use the shared safe HTTP helper: `pkg/http/safe``safe.Download(destPath, url, *safe.Options)`. Default policy: only `http/https`, enforced timeouts and max size, writes to a `0600` temp file then renames.
- SSRF protection (mandatory unless explicitly needed for tests): set `AllowPrivate=false` to block private/loopback/multicast/link-local ranges. All redirect targets are validated and the final connected peer IP is also checked. Prefer an image-focused `Accept` header for image downloads.
- Avatars and small images: use `internal/thumb/avatar.SafeDownload` (15 s timeout, 10 MiB, `AllowPrivate=false`).
- Tests using `httptest.Server` on 127.0.0.1 must pass `AllowPrivate=true` explicitly.
- Keep per-resource size budgets small; rely on `io.LimitReader` + `Content-Length` prechecks.

View file

@ -12,19 +12,4 @@
> Quick Tip: to inspect GitHub issue details without leaving the terminal, run `curl -s https://api.github.com/repos/photoprism/photoprism/issues/<id>`; if `gh` is set up, you MAY also run `gh issue view <id> -R photoprism/photoprism`.
## Acceptance Tests
- Use the `acceptance-*` targets in the `Makefile`.
- For one-off checks, run a single TestCafe case by `testID`:
```bash
make storage/acceptance
make acceptance-sqlite-restart
make wait-2
(cd frontend && npm run testcafe -- "chrome --headless=new --use-gl=angle --use-angle=swiftshader --disable-features=LocalNetworkAccessChecks" --config-file ./testcaferc.json --test-meta mode=public,type=short,testID=components-001 "tests/acceptance")
make acceptance-sqlite-stop
```
- If your command temporarily changes into `frontend/`, run `make acceptance-sqlite-stop` after returning to the repository root.
## Hidden Error UI
Hidden error UI checks for the hidden route require both `files.file_error` and `photos.photo_quality = -1`; hidden searches are quality-gated, so setting only `file_error` will not surface the row.
> Frontend test sequences (Vitest / TestCafe / Playwright), hidden-route UI gotchas, and acceptance-test flow live in `frontend-rules.md`.