Compare commits

..

No commits in common. "develop" and "260305-fad9d5395" have entirely different histories.

1865 changed files with 63535 additions and 173540 deletions

2
.agents/.gitignore vendored
View file

@ -1,2 +0,0 @@
*
!.gitignore

8
.claude/.gitignore vendored
View file

@ -1,8 +0,0 @@
*
!.gitignore
!CLAUDE.md
!rules
!rules/*.md
!agents
!agents/*.md
!settings.json

View file

@ -1,148 +0,0 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. Detailed rules are in `.claude/rules/*.md` files organized by topic.
## Build Commands
Run `make help` to list all available targets. Key commands:
**Backend (Go):**
- `make build-go` — build the `photoprism` binary (develop mode)
- `make build-all` — build backend + frontend
- `go build ./...` — compile all Go packages
**Frontend (Vue 3):**
- `make build-js` — production build of the frontend
- `make watch-js` — watch mode for frontend development (Ctrl+C to stop)
**Dependencies:**
- `make dep` — install all dependencies (TensorFlow models, ONNX models, JS packages)
- `make dep-js` — install JS dependencies only (`npm ci`). The `photoprism/develop` image and the repo `Makefile` both set `NPM_CONFIG_IGNORE_SCRIPTS=true`, so install scripts are skipped automatically; when running npm directly in an env without that default, pass `--ignore-scripts`. Rebuild native addons with `npm rebuild --ignore-scripts=false <pkg>` — a bare `npm rebuild` no-ops wherever the env default is active.
**Docker dev environment:**
- `make docker-build` — build local Docker image
- `docker compose up` — start dev environment (app at http://localhost:2342/)
- `make terminal` — open shell in dev container
## Testing
**Run all tests:**
- `make test` — runs both JS and Go tests
- `make test-go` — all Go tests (slow, ~20 min)
- `make test-js` — frontend unit tests (Vitest)
- `make test-short` — short Go tests in parallel (~5 min)
**Run targeted Go tests:**
```bash
go test ./internal/api -run 'TestFunctionName' -count=1
go test ./internal/photoprism -run 'TestMediaFile_' -count=1
go test ./internal/entity/... -count=1 -tags="slow,develop"
```
**Run targeted JS tests:**
- `make vitest-watch` — Vitest in watch mode
- `make vitest-coverage` — Vitest with coverage report
**Reset test databases before running Go tests:**
- `make reset-testdb` — clears SQLite test DBs and MariaDB testdb
**Subset targets:** `make test-pkg`, `make test-api`, `make test-entity`, `make test-commands`, `make test-photoprism`, `make test-ai`
## Formatting & Linting
Available targets: `make fmt` (everything), `make fmt-go`, `make fmt-js`, `make fmt-swag` / `make swag` (Swagger), `make lint-go`, `make lint-js`. Detailed conventions live in `.claude/rules/go-code-style.md` and `.claude/rules/frontend-rules.md`.
When creating or editing shell scripts, run `shellcheck <file>` and resolve warnings. When editing Markdown files that contain tables, format them with `npx --yes markdown-table-formatter <filename>`.
## Schema Migrations
If a change touches database schema, check migrations:
```bash
go run cmd/photoprism/photoprism.go migrations ls
go run cmd/photoprism/photoprism.go migrations run
# or via Makefile:
make migrate
```
Migration files live in `internal/entity/migrate/`.
## Architecture Overview
PhotoPrism is a self-hosted photo management app. The backend is Go, the frontend is Vue 3 + Vuetify 3, and the database is MariaDB or SQLite (via GORM).
### Backend (`internal/`, `pkg/`, `cmd/`)
| Package | Purpose |
|-------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `internal/photoprism` | **Core application logic**: indexing originals, metadata extraction, thumbnail generation, import/stacking, converter orchestration (FFmpeg/ImageMagick/ExifTool). Entry point for workers and CLI. |
| `internal/entity` | **Database models** (GORM): Photo, File, Album, Label, Face, User, Session, etc. Contains fixtures for tests and migration helpers. |
| `internal/entity/query` | Database query helpers used by the API and core packages. |
| `internal/api` | **REST API handlers** (Gin): thin handlers that validate input, enforce ACL/auth, delegate to services. Annotated with Swagger comments. |
| `internal/server` | HTTP server setup, routing (Gin engine), WebDAV, static assets, middleware wiring. Routes are registered in `routes.go`. |
| `internal/config` | Application configuration: CLI flags, env vars, client config sent to the frontend. |
| `internal/workers` | Background workers: indexing scheduler, metadata sync, sharing, backup, vision jobs. |
| `internal/commands` | CLI command implementations (`github.com/urfave/cli/v2`). |
| `internal/auth` | Authentication: ACL (`auth/acl`), JWT (`auth/jwt`), OIDC (`auth/oidc`), session management. |
| `internal/form` | Request form/binding structs for the API layer. |
| `internal/meta` | Metadata extraction from EXIF, XMP, JSON sidecars. |
| `internal/ffmpeg` | FFmpeg/transcoding helpers. |
| `internal/thumb` | Thumbnail generation helpers. |
| `internal/ai` | AI/vision model integration (TensorFlow, ONNX). |
| `internal/service` | Services: maps geocoding, hub (membership), cluster, WebDAV client, CIDR helpers. |
| `internal/event` | Event bus for structured logging and audit events. |
| `pkg/` | Standalone, reusable packages: `fs`, `geo`, `media`, `txt`, `clean`, `rnd`, `i18n`, `http`, `time`, etc. No dependency on `internal/`. |
**Request flow:** HTTP request → Gin middleware (auth, rate limiting) → `internal/api` handler → `internal/photoprism` or `internal/entity` → response.
**Audit logging convention** (`event.AuditInfo/Warn/Err`): slices must follow the pattern **Who → What → Outcome**:
- Who: `ClientIP(c)` + actor context (`"session %s"`, `"user %s"`)
- What: resource constant + action segments
- Outcome: single token like `status.Succeeded`, `status.Failed`, `status.Denied`, `status.Error(err)`
### Frontend (`frontend/`)
Vue 3 app using the Options API and Vuetify 3.
| Directory | Purpose |
|---------------------------|-------------------------------------------------------------------------------------------------|
| `frontend/src/model/` | Client-side models mirroring API responses (Photo, Album, File, User, etc.) |
| `frontend/src/app/` | App bootstrap, routing (`routes.js`), and the `$session` reactive singleton |
| `frontend/src/page/` | Page-level components |
| `frontend/src/component/` | Reusable UI components |
| `frontend/src/common/` | Shared utilities, the API client (`$api`), and reactive singletons (`$config`, `$view`, `$log`) |
| `frontend/src/locales/` | i18n translation files |
| `frontend/tests/` | Vitest unit tests + TestCafe acceptance tests |
State management uses reactive singleton modules in `src/common/` and `src/app/`, not Vuex or Pinia. Frontend code-style, formatting, testing, translation, and Playwright rules live in `.claude/rules/frontend-rules.md`.
### API Conventions
- REST API v1 base path: `/api/v1/` (configured via `conf.BaseUri()`)
- Authentication: Bearer token (`Authorization` header) or `X-Auth-Token` header
- Pagination: `count`, `offset`, `limit` parameters (default 100, max 1000)
- After adding/changing API handlers, regenerate Swagger docs: `make fmt-go swag-fmt swag`
- New routes must be registered in `internal/server/routes.go`
### Config & Flags
Verify config option names before using them:
```bash
./photoprism --help
./photoprism show config-options
./photoprism show config-yaml
```
### Verify Before Propagating
Before promoting a claim from `CLAUDE.md`, `AGENTS.md`, a memory entry, or another spec into a new rule, spec, code comment, or commit message, verify it against the current code (grep imports, list directories, read the cited file). Stale documentation silently turns into stale rules and stale specs that future sessions will trust. When the claim names a package, function, file, or framework, the cost of one grep is much smaller than the cost of repeating an error across multiple files.
### Detailed Rules
Topic-specific conventions live under `.claude/rules/` and are loaded alongside this file:
- `code-comments.md` — shared JS/Go doc comment rules (length cap, what to omit); referenced by both style files.
- `go-code-style.md`, `go-testing.md` — Go style, package boundaries, test patterns, fixtures.
- `frontend-rules.md` — JS/Vue code style, formatting, dependencies, tests, Playwright, translations.
- `commit-and-docs-style.md` — commit-message format, GitHub issue templates, spec heading style.
- `safety-and-security.md` — Git/data safety, destructive commands, file I/O and archive-extraction policies, HTTP download helpers.
- `api-and-config.md`, `cluster-operations.md`, `import-index-download.md`, `build-and-runtime.md`, `sources-of-truth.md` — domain-specific guidance.

View file

@ -1,41 +0,0 @@
---
name: ui-tester
description: Drives the Playwright MCP browser to exercise UI flows, verify behavior in a real Chromium, and report findings concisely. Use for any task that needs to navigate pages, click through flows, fill forms, capture console errors, check network requests, or validate UI state. Returns a short verdict + evidence so the parent context isn't filled with raw snapshots and console logs.
tools: Bash, Read, Grep, Glob, WebFetch, ToolSearch, mcp__playwright__browser_navigate, mcp__playwright__browser_navigate_back, mcp__playwright__browser_snapshot, mcp__playwright__browser_take_screenshot, mcp__playwright__browser_click, mcp__playwright__browser_hover, mcp__playwright__browser_drag, mcp__playwright__browser_drop, mcp__playwright__browser_type, mcp__playwright__browser_fill_form, mcp__playwright__browser_select_option, mcp__playwright__browser_press_key, mcp__playwright__browser_file_upload, mcp__playwright__browser_handle_dialog, mcp__playwright__browser_wait_for, mcp__playwright__browser_console_messages, mcp__playwright__browser_network_requests, mcp__playwright__browser_evaluate, mcp__playwright__browser_run_code, mcp__playwright__browser_resize, mcp__playwright__browser_tabs, mcp__playwright__browser_close
---
You are a focused UI test driver. The parent agent has delegated browser-driven work to you so its context stays clean. Your output is the only thing it sees — make it short, structured, and decision-ready.
## Loading tool schemas
The Playwright MCP tools above appear in your toolset by name but their schemas are deferred. Before you can call any of them, run `ToolSearch` once with `select:<tool_names>` to load the schemas for the specific tools you need (e.g. `select:mcp__playwright__browser_navigate,mcp__playwright__browser_snapshot,mcp__playwright__browser_click`). Don't load all of them — just the handful this task actually needs.
## Browser environment
- The MCP server runs **one shared Chromium instance** with a **persistent profile**. Cookies and `localStorage` from earlier sessions persist. Assume state may already exist — log in fresh if a flow requires a known account.
- This profile is **separate** from the user's real Chrome. You won't see their personal logins or extensions.
- There is no parallelism — only you should be driving the browser during your run.
## Workflow
1. **Plan first.** Decide the minimum flow that proves or disproves the parent's question. Don't explore; execute.
2. **Run.** Navigate, interact, observe. Prefer `browser_snapshot` (accessibility tree) over `browser_take_screenshot` — it's much smaller and works for assertions. Reach for screenshots only when the parent explicitly wants visual evidence.
3. **Capture signals.** Pull `browser_console_messages` and, if relevant, `browser_network_requests` near the end of the flow — they're often the actual answer.
4. **Clean up before returning.** Call `browser_close` so the next subagent starts with no open tabs. The persistent profile keeps cookies, but tabs/state shouldn't leak across runs.
## Reporting format
Default to **under 300 words** unless the parent asked for more. Structure:
- **Verdict:** one sentence — pass / fail / partial, and what was tested.
- **Evidence:** 36 bullet points with the concrete observations (URLs visited, elements clicked, console errors verbatim, network failures with status codes). Quote error messages exactly; don't paraphrase.
- **Notes (optional):** anything the parent should know that wasn't asked for but matters (e.g. "noticed an unrelated 404 on `/api/foo`").
Don't paste full snapshots, full console logs, or screenshot data unless the parent specifically requested them. The parent does not need to see your tool-call play-by-play — just the conclusion and the evidence supporting it.
## What not to do
- Don't open new browsers, install Playwright, or modify the MCP server config.
- Don't edit application code unless the parent explicitly told you to. Your job is to test what's there.
- Don't run long-form Go/JS test suites (that's `make test` territory) — you're the manual-QA-in-a-browser agent, not a CI runner.
- Don't summarize what you just did at the end ("I navigated to X, then clicked Y, then..."). The verdict + evidence is the summary.

View file

@ -1,42 +0,0 @@
## API & Config Changes
- Respect precedence: `options.yml` overrides CLI/env values, which override defaults.
- 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.
- Adding a `customize.FeatureSettings` flag: a new field defaults to `true` via reflection (`features_default.go`) and is operator-disableable through `PHOTOPRISM_DISABLE_FEATURES` — no new CLI option needed. It cascades: update the full-struct literals in `internal/config/customize/{acl,scope}_test.go` and `internal/config/client_config_test.go` (the longest field name re-aligns every literal via gofmt), and `testdata/settings.yml` self-updates via `TestSettings_Save`. If the flag is only meaningful for accounts/roles, gate it per-session in `customize.Settings.ApplyACL` / `ApplyScope` (e.g. `Account`/`AppPasswords` require `ResourcePassword`/`ActionUpdate`); that shapes the Web UI client config only — enforce server-side behavior on the global flag via a `Config.DisableX()` helper.
- Identify an app-password credential by its session, not its token: `(*entity.Session).IsApplication()` (auth provider `application`) covers every grant that mints one (`password`/`session`/`cli`). The token format and grant type vary, so don't gate on `rnd.IsAppPassword` or `GrantType`.
- 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`. 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
When renaming or adding fields:
- Field casing: **TitleCase** (`UUID`, `Name`, `SiteUrl`) for fields backed by a DB entity (mirror the entity/model), **camelCase** (`storageNamespace`, `redirectUri`) for generated/artificial payloads (client config, session, action/RPC bodies). A filtered/computed entity projection stays TitleCase; an action payload stays camelCase but MAY TitleCase its single entity-identity field (e.g. `UUID`). See `specs/common/field-casing.md`.
- Update DTOs in `internal/service/cluster/response.go` and any mappers.
- Update handlers and regenerate Swagger: `make fmt-go swag-fmt swag`.
- Update tests (search/replace old field names) and examples in `specs/`.
- Quick grep: `rg -n 'oldField|newField' -S` across code, tests, and specs.
## 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)`.
- 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`; default unknown client roles to `RoleClient`.
- Build CLI role help from the registered role map (never hand-maintained literals) so each edition lists exactly the roles it accepts: `commands.UserRoleUsageFor(<map>)` / `Roles.CliUsageString()`, passing CE `acl.UserRoles` or an edition's own static `auth.UserRoles` (reference the edition map directly, not the runtime-reassigned `acl.UserRoles`, to avoid the init-order trap; Portal's map includes `cluster_admin`). For federatable / cluster-instance contexts (LDAP, OIDC group→role, cluster grants) use `acl.ClusterInstanceRolesCliUsageString()` — it excludes `cluster_admin`/`visitor`. `pkg/txt.JoinOr` renders the "a, b, or c" style.
- For JWT/client scope checks, use the shared helpers (`acl.ScopePermits` / `acl.ScopeAttrPermits`).

View file

@ -1,46 +0,0 @@
## Agent Runtime (Host vs Container)
Agents MAY run either inside the Development Environment container (recommended) or on the host.
### Detecting the Environment
- **Inside container if** `/.dockerenv` exists (authoritative signal).
- Path hint: when the project path is `/go/src/github.com/photoprism/photoprism` *and* `/.dockerenv` is absent, assume you are on the host with a bind mount.
### Host Mode
- Build local dev image (once): `make docker-build`
- Start services: `docker compose up` (add `-d` for background)
- Follow live app logs: `docker compose logs -f --tail=100 photoprism`
- Execute a single command in the app container: `docker compose exec photoprism <command>`
- Run as non-root to avoid root-owned files: `docker compose exec -u "$(id -u):$(id -g)" photoprism <command>`
- Open a terminal session: `make terminal`
- Stop everything: `docker compose --profile=all down --remove-orphans` (`make down`)
### Container Mode
- Install deps: `make dep`
- Build frontend/backend: `make build-js` and `make build-go`
- Watch frontend changes: `make watch-js`
- Start PhotoPrism server: `./photoprism start`
- HTTP: http://localhost:2342/
- HTTPS: https://app.localssl.dev/ (via Traefik, if running)
- Admin Login: Default credentials are `admin` / `photoprism`; check `compose.yaml` for `PHOTOPRISM_ADMIN_USER` and `PHOTOPRISM_ADMIN_PASSWORD` if they differ.
- Do not use the Docker CLI inside the container; starting/stopping services requires host Docker access.
### Operating Systems & Architectures
- Guides and command examples assume Linux/Unix shell on 64-bit AMD64 or ARM64.
- For Windows-specifics, see the Developer Guide FAQ: https://docs.photoprism.app/developer-guide/faq/#can-your-development-environment-be-used-under-windows
### CLI Binary Names
The CLI name is `photoprism` in production and public docs. Other binary names (`photoprism-plus`, `photoprism-pro`, `photoprism-portal`) are only used in development builds for side-by-side comparisons.
## Container Image Builds
- **Pin the dev `Dockerfile` `FROM` to a dated `photoprism/develop` tag, never the floating codename.** Use `photoprism/develop:<YYMMDD>-<codename>` (e.g. `photoprism/develop:260520-resolute`), not `photoprism/develop:resolute` / `:latest` / `:ubuntu`. The dated tag pins to a known-good build so a drive-by `docker pull` doesn't silently swap in a fresh base mid-session, and bumping the date in a commit gives developers + reviewers a visible signal that a new base image is available. The `buildx-multi.sh` script publishes both `:<codename>` and `:<YYMMDD>-<codename>` on every build — the dated one is the durable pointer. When you bump the base image, also update `Dockerfile`, the host `compose.yaml` (if it references the tag explicitly), and any docs that quote the current dev image.
- **Never mix Debian and Ubuntu `apt` repositories in the same image:**
- Don't add a Debian source to an Ubuntu base (or vice versa) to install a single missing package — the transitive deps drift, apt's solver pulls newer libraries from the foreign distro, and other build steps in the same `RUN` (e.g. `install-libheif.sh` running `apt-get install libavcodec-dev`) silently link against the wrong soname.
- Symptoms surface much later as `dlopen: libfoo.so.N: cannot open shared object file` at image runtime, with the binary referencing a soname that exists only in the foreign distro.
- If a package isn't available in the host distro's repos, prefer (a) a same-distro PPA / backports source, (b) a vendor-supplied .deb (e.g. Google Chrome from `dl.google.com`), or (c) a from-source build pinned to a known version.

View file

@ -1,41 +0,0 @@
## Cluster Operations
- 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}`); 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).
### Provisioner & DSN
- 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`.
- 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 for all roles. Client config includes `storageNamespace` (SHA-256 of `SiteUrl`) for browser storage scoping.
### Preflight Checklist
- `go build ./...`
- `make fmt-go swag-fmt swag`
- `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: `make swag` may fetch modules — confirm network access before running.

View file

@ -1,12 +0,0 @@
## JS/Go Code Comment Rules
A doc comment is **required** for every function (including unexported helpers), as well as for every non-trivial Vue `methods:` / `computed:` / watcher:
- Keep comments **compact** and default to one line for "what" in the format `// Name does X.`. Skip trivial getters (`isOpen: () => this.open`).
- Add 1-2 follow-up lines (`// …`) **only** if the "why" is non-obvious: a hidden invariant, a workaround that would otherwise be undone by a future cleanup, a contract a reader can't infer from the code. If readers can infer the "why" from the function body or a nearby line, then omit it.
- Multi-paragraph explanations belong in `specs/`, package `README.md` files, or GitHub issues — never in the source itself.
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. For short examples in comments, indent code instead of using backticks.
Use US English spelling in all code comments (`parameterized`, `behavior`, `color`, `serialize`, `normalize`, `optimize`, …) — not the British `-ised`/`-our`/`-re` variants.
> **Don't include in code comments:** Issue / PR numbers, "previously…" history, alternatives considered, what the function used to do, references to old commits, names of subsequent reviewers, or any narrative that names the change rather than the steady-state behavior. That context belongs in commit messages, specs, or handover notes.

View file

@ -1,57 +0,0 @@
## Commit Messages
Use concise, imperative subjects with a one-word prefix indicating the scope or topic:
- `Config: Add tests for "darktable-cli" path detection`
If the commit relates to specific issues or pull requests, reference their IDs in the message:
- `Docker: Use two stage build to reduce image size #123 #5632`
Commit messages must not exceed 80 characters in length.
Do not add `Co-Authored-By: Claude …` trailers (or any other AI-authorship trailer) to commit messages.
## GitHub Issues
Issue titles MUST be concise, use the imperative mood, and start with a single capitalized prefix followed by a colon and a space, e.g. `Search: Add filter for RAW image formats`.
Issue descriptions MUST begin with a one-sentence **User Story** in the format: `**As a <role>, I want <goal>, so that <outcome>.**`
Use level-3 Markdown headings for sections within issue descriptions, for example `### Acceptance Criteria`.
Follow the User Story with a clear summary of the expected behavior, rationale, technical considerations, and constraints.
Descriptions MUST conclude with a checklist of **Acceptance Criteria**:
- Use GitHub checklist formatting: `- [ ]`
- Criteria MUST be clear, testable, and unambiguous.
- Each item MUST use one of the following requirement-level keywords:
- `MUST` — required for the issue to be considered complete
- `SHOULD` — strongly recommended but not strictly required
- `MAY` — optional enhancement
- Keep the checklist current: once the work for a criterion is implemented **and verified**, mark it done (`- [x]`).
- Leave items that are unverified, not yet implemented, or skipped optional (`MAY`) enhancements unchecked.
- An issue is complete only when every `MUST` is checked; never tick a box on the strength of a plan alone or an unrun test.
- When referencing an issue from a commit that fulfills some of its criteria, update the matching boxes first.
> Agents MUST create, edit, close, reopen, relabel, or otherwise modify GitHub issues only when explicitly requested by the user.
The repo's issue templates use the new GitHub `type:` property (`Bug`, `Feature`) instead of `bug`/`idea` labels. `gh issue create` does not yet accept a `--type` flag, so when filing issues programmatically use `--label` only and tell the user to set the issue type via the web UI.
## Specifications & Documentation
- Document headings use a **Chicago-style title case**, with additional code- and path-aware normalization rules (see below). Always spell the product name as `PhotoPrism`.
- When writing CLI examples or scripts, place option flags before positional arguments unless the command requires a different order.
- Use RFC 3339 UTC timestamps in request and response examples, and valid ID, UID and UUID examples in docs and tests.
- Technical specifications in the nested `specs/` subrepository may not be present in every clone or environment. Do not add `Makefile` targets in the main project that depend on `specs/` paths.
- Auto-generated configuration and command references live under `specs/generated/`. Agents MUST NOT read, analyze, or modify anything in this directory.
- Nested Git repositories may appear to be ignored; if so, change directories before staging or committing updates.
- **Never reference `specs/` paths from public artifacts** — issue bodies, PR descriptions, package READMEs (`frontend/README.md`, `internal/*/README.md`, etc.), top-level `CODEMAP.md`/`GLOSSARY.md`, code comments outside `specs/`. External readers see a 404 and the private subrepo's existence is leaked. Hints in `AGENTS.md` and `CLAUDE.md` files are the documented exception. Quick check: `grep -n "specs/" <file>` should return no matches before saving any public-facing file.
> **Title Case** rules (Chicago-style headline capitalization, with code- and path-aware normalization):
> - Capitalize the first word, the first word after a colon, dash, or end punctuation, and all major words, including the second part of a hyphenated major word.
> - Lowercase only articles, short conjunctions, and short prepositions of three letters or fewer when they are not in one of those positions.
> - Preserve known acronyms (for example, API, CLI, HTTP, JSON) and slash-separated acronym groups (for example, CSV/TSV) as uppercase.
> - Preserve RFC 2119 / RFC 8174 normative keywords (MUST, SHOULD, MAY, SHALL, REQUIRED, RECOMMENDED, OPTIONAL) as uppercase when used in their normative sense.
> - Preserve inline code spans (`` `foo` ``), file paths (e.g. `docs/foo-bar.md`), and slash commands (e.g. `/grill-me`) verbatim; do not recase their contents.
> - Use `&` instead of `And`/`Or` in headings.
> Refresh the `**Last Updated:**` date at the top of documents whenever you make changes to their contents, using the format `January 20, 2026` (without time); leave it as-is for simple formatting or whitespace-only edits.

View file

@ -1,77 +0,0 @@
## Frontend Code Style & Test Coverage
- **Comments:** Follow the code comment rules in `code-comments.md`.
- **Tests:** Test new JS functions (including helpers) and new Vue components whenever practical; update existing tests when behavior changes. When a unit test is impractical (DOM-heavy flows, third-party widget integration), the doc comment is still mandatory — it's the minimum bar.
- **State:** Shared state lives in reactive singleton modules under `src/common/` and `src/app/` (e.g., `app/session.js`, `common/config.js`, `common/clipboard.js`, `common/log.js`) that export a `reactive()` / `ref()` object directly; components access them via `import` or via the globally-installed `$config` / `$session` plugins. Do not introduce Vuex, Pinia, or new ad-hoc stores — extend an existing singleton or add a new one alongside its peers in `common/` or `app/`.
- **Vue/Vuetify:** Use the Options API in Vue components (consistent with the rest of the codebase); do not introduce Composition API or `<script setup>`.
- **TypeScript:** Do not introduce TypeScript. The frontend is a pure JS + Vue SFC codebase: no `.ts` files, no `tsconfig.json`, no `<script lang="ts">` blocks. JSDoc type annotations in comments are fine; full TS migrations are out of scope.
## Frontend Formatting
- ESLint + Prettier own formatting. After edits run `make fmt-js` (or `npm run fmt` inside `frontend/`) and `make lint-js` to verify; `frontend/eslint.config.mjs` is the flat-config source of truth.
- The dev container preinstalls `eslint` and `prettier` on the global `PATH` at the same version `frontend/package.json` pins (`eslint --version` should match the `eslint` entry in `frontend/package.json`). Invoke them directly (e.g. `eslint --fix tests/`) from `frontend/` — no need for `npx`, which adds a spawn step and an extra resolution layer.
- Prettier reflow is **not** part of `make fmt-js` / `eslint --fix` — the `prettier/prettier` rule is set to `"off"` so intentional newlines (multi-line method chains, vertical predicate lists) are preserved. Run `prettier --write <file>` explicitly when a full reflow is wanted; do not run it blanket across `src/` or `tests/`.
- Prettier uses `printWidth: 160`, double quotes, semicolons, `trailingComma: "es5"`, and `proseWrap: "never"` (see `frontend/.prettierrc.json`). Do not hand-wrap long lines — let Prettier decide. CSS/SCSS use `tabWidth: 4`.
- The repo-root `.editorconfig` covers indentation and newline style; don't override it locally.
- Vue SFC block order is `<template>``<script>``<style>`; keep it consistent with existing components.
## Frontend Dependencies & Pins
- `frontend/README.md` is the canonical doc for pin rationale, the `overrides` layer, ESM-only upgrade blockers, and the orphan-audit pattern — read it before bumping any non-caret pin or adding/removing a top-level dep.
- **Pins are intentional.** When a version has no caret (e.g., `"axios": "1.16.1"`, `"vuetify": "3.12.2"`), check `frontend/README.md` and `git log -p -S "<pkg>" -- frontend/package.json` for the reason before changing it.
- npm is a workspace; run `npm install --ignore-scripts --no-audit --no-fund --no-update-notifier` from the **repo root** (not `frontend/`) so the root `package-lock.json` updates. After dep changes also run `make audit`, `make build-js`, `make test-js`, and `make notice`.
- Before adding a new dep or removing one as "unused", run `rg -nF "<pkg>" frontend ...` plus `npm ls <pkg> --all` to confirm there's no transitive consumer or peer-dep. Recent precedents: `postcss-url`, `@vitejs/plugin-react`, `cheerio`, `@testing-library/react`, `vite-tsconfig-paths` (all true orphans removed once consumer left).
## Frontend Linting & Test Entry Points
- 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`.
- **Always invoke Vitest through the npm/make wrapper, never bare `npx vitest run`.** `frontend/package.json`'s `test` script wraps the call in `cross-env TZ=UTC BUILD_ENV=development NODE_ENV=development BABEL_ENV=test`. Without those env vars ~50 component tests (Vuetify renders, chip-selector, login, location-input, batch-edit, people-tab, lightbox `toggleSidebar`, etc.) and TZ-sensitive date tests fail spuriously — the failures look real but only reproduce in the unwrapped invocation. Do not compare a "failed N, passed M" report from bare `npx vitest run` against a `make test-js` baseline. For ad-hoc filtering on a single file, mirror the env explicitly: `(cd frontend && TZ=UTC BUILD_ENV=development NODE_ENV=development BABEL_ENV=test npx vitest run <path>)`.
- 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
- Hidden-route UI checks under `/library/hidden` or `/portal/hidden` require both `files.file_error` and `photos.photo_quality = -1`; `file_error` alone will not surface the row.
## Playwright MCP Usage
- Endpoint `http://localhost:2342/`; logins at `/library/login` (CE/Plus/Pro) and `/portal/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.
- Delegate to the `ui-tester` subagent for any flow with more than ~2 browser steps (login + navigate + assert, multi-step forms, regression sweeps). Brief it with the URL, credentials, exact steps, and the verdict format you want back; ask for a short report so raw snapshots and console dumps stay out of the parent context. Drive Playwright MCP inline only for one-shot checks (single navigate, single screenshot).
## Frontend Focus Management
- 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
- Never hardcode locale strings in templates or scripts — every user-visible string MUST go through `$gettext` / `T` so it appears in `frontend/src/locales/translations.pot`.
- **Exception — standardized technical identifiers stay untranslated.** Render protocol/acronym/identifier field labels and option values as literal strings, not via `$gettext`: e.g. `Client ID`, `Client Credentials`, `OIDC`, `UUID`, `Node UUID`. Translating them adds catalog noise and risks ambiguous renderings (e.g. "Client" → German "Kunde"). Common English field names like `Site URL` or `Advertise URL` stay translated.
- **Share role/provider labels, not the selectable lists.** Display names live once in `frontend/src/options/auth.js` (`Roles()` / `Providers()` maps, with `RoleOptions(keys, labelKey)` / `ProviderOptions(keys, labelKey)` builders). Private editions (`plus`/`pro`/`portal`) import these and pass their own key list — the selectable sets legitimately differ per edition (cluster_admin, LDAP/AD, reduced Plus set) but the labels must not be re-listed.
- 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`. Feature PRs do **not** commit the resulting `.pot`/`.po` churn — Weblate re-extracts on its refresh cycle (see `specs/frontend/translations.md`); a dedicated "Regenerate translation catalogs" commit is the exception.
- Avoid punctuation-only gettext keys (e.g. `$gettext("—")`) — they clutter `frontend/src/locales/translations.pot`.
- Catalog integrity gate: run `make gettext-lint` (`scripts/gettext-lint.mjs`) after any `.po`/`.pot` edit. It flags placeholder-set mismatches between `msgid` and `msgstr` (frontend `%{name}`, backend printf verbs), edge-whitespace drift, and `msgfmt -c` c-format fatals — defects that silently break variable substitution at runtime. Leading/trailing/internal whitespace inside a `msgid` is itself runtime-inert (`vue3-gettext` trims + collapses keys on load and lookup), so trailing-space findings are cleanliness, not correctness.
- Trimming a source string's trailing space must stay consistent across template + `.po` + `.pot`: trim the literal, then `make gettext-extract`. Caveat: `msgmerge --no-fuzzy-matching` treats the trimmed `msgid` as new, blanks its `msgstr`, and moves the old translation to an obsolete `#~` block — so carry that translation onto the active trimmed entry (collapse any folded multi-line `#~ msgstr`) before `make gettext-compile`, or every locale renders untranslated. This only fills `msgstr`, so Weblate merges it cleanly.
## Web Templates & Shared Assets
- 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,31 +0,0 @@
## Go Code Style
- **Comments:** Follow the code comment rules in `code-comments.md`.
- **Packages:** 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.
- **Format:** Go is formatted by `gofmt` with tabs. Do not hand-format indentation. After edits run `make fmt-go` (gofmt + goimports).
- **Linting:** Run `make lint-go` (`golangci-lint`) after Go changes; prefer `golangci-lint run ./internal/<pkg>/...` for focused edits.
## Package Boundaries
- 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 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
- Always use shared permission variables from `pkg/fs` when creating files/directories:
- Directories: `fs.ModeDir` (0o755 with umask)
- Regular files: `fs.ModeFile` (0o644 with umask)
- Config files: `fs.ModeConfigFile` (default 0o664)
- Secrets/tokens: `fs.ModeSecretFile` (default 0o600)
- Backups: `fs.ModeBackupFile` (default 0o600)
- 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.
- 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,49 +0,0 @@
## Go Test Coverage
- 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
- Tests live next to sources (`<file>_test.go`); group cases with `t.Run(...)` using **PascalCase** names (`Success`, `InvalidRequest`). Consecutive subtests inside the same `Test*` function are written without blank lines between them so the cases read as a compact table; reserve blank lines for separating distinct setup blocks.
- 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
- 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 builders (moderate): `go test ./internal/ffmpeg -run 'Remux|Transcode|Extract' -count=1`
### Test Config Helpers
- 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
- `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` 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 & Hardware Gating
- 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
- 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,26 +0,0 @@
## Import/Index
- 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/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/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:** 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 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 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,32 +0,0 @@
## Safety & Data
- 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. Nested subrepos (e.g. `specs/`) may lack a configured committer identity — pass `-c user.email=… -c user.name=…` to the specific `git commit` invocation rather than configuring the repo.
- Do not run destructive commands against production data. Prefer ephemeral volumes and test fixtures for acceptance tests. The destructive CLI commands `photoprism reset`, `users reset`, `auth reset`, and `audit reset` require explicit `--yes`; never invoke them in examples or scripts without a backup warning.
- 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 `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.
## Archive Extraction — Security Checklist
- 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: 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

@ -1,16 +0,0 @@
## Sources of Truth
- Makefile targets (always prefer existing targets): see the `Makefile`
- Developer Guide Setup: https://docs.photoprism.app/developer-guide/setup/
- Developer Guide Tests: https://docs.photoprism.app/developer-guide/tests/
- Contributing: `CONTRIBUTING.md`
- Security: `SECURITY.md`
- REST API: https://docs.photoprism.dev/ (Swagger), https://docs.photoprism.app/developer-guide/api/ (Docs)
- Code Maps: `CODEMAP.md` (Backend/Go), `frontend/CODEMAP.md` (Frontend/JS)
- Terminology Glossary: `GLOSSARY.md` (single source for term definitions across specs/docs)
- Package-level `README.md` files under `internal/`, `pkg/`, `frontend/`, and `frontend/src/` for detailed package documentation.
- Frontend dependency pin rationale, override layer, and orphan-audit pattern: `frontend/README.md` (check before bumping any non-caret pin or adding/removing a top-level dep).
> 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`.
> Frontend test sequences (Vitest / TestCafe / Playwright), hidden-route UI gotchas, and acceptance-test flow live in `frontend-rules.md`.

View file

@ -1,19 +0,0 @@
{
"permissions": {
"additionalDirectories": ["/go", "/tmp", "/home"],
"allow": [
"Bash(make:*)",
"Bash(go:*)",
"Bash(npx --yes markdown-table-formatter:*)",
"Read",
"Write",
"Edit",
"Grep",
"Glob",
"WebSearch",
"mcp__playwright__*"
],
"defaultMode": "acceptEdits"
},
"awaySummaryEnabled": false
}

View file

@ -14,7 +14,6 @@
/assets/models/
/storage/
/build/
/setup/Makefile
/photoprism
/photoprism-*
/frontend/tests/acceptance/screenshots/
@ -70,7 +69,6 @@ venv
.nv
.eslintcache
.gocache
.gitconfig
.DS_Store
.DS_Store?
._*
@ -95,5 +93,6 @@ Thumbs.db
*.override.md
/AGENTS_*
/REPOS.md
/CLAUDE.md
join_token
client_secret

View file

@ -1,41 +0,0 @@
## Build & Test Environment Overrides
##
## Copy this file to ".env" in the same directory as compose.yaml and uncomment
## the variables you want to customize.
# Isolates container names, named volumes, and the auto-prefixed default network
# across parallel checkouts of this repo. To run multiple environments side-by-
# side, set this together with COMPOSE_NETWORK_NAME below.
# COMPOSE_PROJECT_NAME=photoprism
# Bridge-network name. Override together with COMPOSE_PROJECT_NAME to run
# multiple parallel dev environments without colliding on the shared network.
# COMPOSE_NETWORK_NAME=photoprism
# Optional services to start by default. Comma-separated list of profile names
# read by Docker Compose itself (so no --profile flag is required).
# COMPOSE_PROFILES=postgres
# In-container working directory and bind mount target for the repo checkout.
# Override when the host clone lives outside the default GOPATH layout; the
# value is reused for the Dockerfile WORKDIR (build arg), working_dir, the
# source bind mount, and every PhotoPrism path env var so they stay in sync.
# WORKING_DIR=/go/src/github.com/photoprism/photoprism
# Network interfaces to which Traefik and other services bind on the host.
# Defaults expose Traefik on every interface (so *.localssl.dev is reachable
# from the LAN) and keep direct service ports on loopback only.
# TRAEFIK_BIND_HOST=0.0.0.0
# SERVICES_BIND_HOST=127.0.0.1
# Host ports for Traefik's HTTP and HTTPS entrypoints.
# TRAEFIK_HTTP_PORT=80
# TRAEFIK_HTTPS_PORT=443
# Public base URL used in share links, OIDC & PWA URIs.
# PHOTOPRISM_SITE_URL=https://app.localssl.dev/
# Ports to which MariaDB and PostgreSQL should bind. PostgreSQL only starts
# when the "postgres" (or "all") profile is enabled via COMPOSE_PROFILES.
# MARIADB_PORT=4001
# POSTGRES_PORT=4002

View file

@ -1,31 +1,26 @@
---
name: Bug Report 🐞
about: Report a new and clearly identified bug that must be fixed directly in the application
title: 'Bug: Edit the title before submitting'
type: Bug
title: 'SHORT DESCRIPTION OF THE PROBLEM YOU ARE REPORTING'
labels: bug
assignees: ''
---
<details>
PLEASE PROCEED ONLY IF YOU ARE SURE THAT THIS IS NOT A TECHNICAL SUPPORT INCIDENT AND/OR POSSIBLY A PROBLEM WITH SOME OTHER SOFTWARE YOU ARE USING:
PLEASE PROCEED ONLY IF YOU ARE ABSOLUTELY SURE THAT THIS IS NOT A TECHNICAL SUPPORT INCIDENT AND/OR POSSIBLY A PROBLEM WITH SOME OTHER SOFTWARE YOU ARE USING. VISIT <https://www.photoprism.app/kb/getting-support> TO LEARN MORE ABOUT OUR SUPPORT OPTIONS. THANK YOU FOR YOUR CAREFUL CONSIDERATION!
1. Thoroughly review our [Getting Started](https://docs.photoprism.app/getting-started/) and [User Guides](https://docs.photoprism.app/user-guide/).
2. Work through the [Troubleshooting Checklists](https://docs.photoprism.app/getting-started/troubleshooting/) we provide.
3. Do not report [known issues](https://docs.photoprism.app/known-issues/) or [missing features](https://github.com/photoprism/photoprism/issues) as bugs.
4. Use [GitHub Discussions](https://github.com/photoprism/photoprism/discussions) for community help, and visit our [Support Guide](https://www.photoprism.app/kb/getting-support/) to learn more about support options.
---------------------------------------------------------------------------
THANK YOU! 💎
</details>
#### 1. What is not working as documented?
**Be as specific as possible and explain which part of the software is not [working as documented](https://docs.photoprism.app/), e.g. "image not found" or "wrong thumbnail" would not be detailed enough.**
Be as specific as possible and explain which part of the software is not [working as documented](https://docs.photoprism.app/), e.g. "image not found" or "wrong thumbnail" would not be detailed enough.
Links to the related documentation on [docs.photoprism.app](https://docs.photoprism.app/):
- ...
*Please never report [known issues](https://docs.photoprism.app/known-issues/) or [missing features](https://github.com/photoprism/photoprism/issues) as bugs, and do not submit bug reports for the purpose of getting [technical support](https://www.photoprism.app/kb/getting-support/) or because you have not received a response in our [public community forums](https://github.com/photoprism/photoprism/discussions). Thank you very much!*
*Please never report [known issues](https://docs.photoprism.app/known-issues/) or [missing features](https://github.com/photoprism/photoprism/issues) as bugs, and do not submit bug reports for the purpose of getting [technical support](https://www.photoprism.app/kb/getting-support) or because you have not received a response in our [public community forums](https://github.com/photoprism/photoprism/discussions). Thank you very much!*
### 1. How can we reproduce it?
#### 2. How can we reproduce it?
Steps to reproduce the behavior:
@ -36,23 +31,23 @@ Steps to reproduce the behavior:
When reporting an import, indexing, or performance issue, please include the number and type of pictures in your library, as well as any configuration options you have changed, such as for thumbnail quality.
### 2. What behavior do you expect?
#### 3. What behavior do you expect?
Give us a clear and concise description of what you expect.
### 3. What could be the cause of your problem?
#### 4. What could be the cause of your problem?
Always try to determine the cause of your problem using the checklists at <https://docs.photoprism.app/getting-started/troubleshooting/> before submitting a bug report.
### 4. Can you provide us with example files for testing, error logs, or screenshots?
#### 5. Can you provide us with example files for testing, error logs, or screenshots?
Please include sample files or screenshots that help to reproduce your problem. You can also email files or share a download link, see <https://www.photoprism.app/contact/> for details.
Please include sample files or screenshots that help to reproduce your problem. You can also email files or share a download link, see <https://www.photoprism.app/contact> for details.
Visit <https://docs.photoprism.app/getting-started/troubleshooting/browsers/> to learn how to diagnose frontend issues.
**Important: Attach or link to files that help us reproduce the problem. Import and indexing issues require sample files and logs.** Otherwise, we will not be able to process your report. If it is an import problem specifically, please always provide us with an archive of the files before you imported them so we can reproduce the behavior.
**Important: If it is an import, indexing or metadata issue, we require sample files and logs from you.** Otherwise, we will not be able to process your report. If it is an import problem specifically, please always provide us with an archive of the files before you imported them so we can reproduce the behavior.
### 5. Which software versions do you use?
#### 6. Which software versions do you use?
(a) PhotoPrism Architecture & Build Number: AMD64, ARM64, ARMv7,...
@ -68,7 +63,7 @@ You can find the version/build number of the app in *Settings* by scrolling to t
*Always provide database and operating system details if it is a backend, import, or indexing issue. Should it be a frontend issue, at a minimum we require you to provide web browser and operating system details. When reporting a performance problem, we ask that you provide us with complete information about your environment, as there may be more than one cause.*
### 6. On what kind of device is PhotoPrism installed?
#### 7. On what kind of device is PhotoPrism installed?
This is especially important if you are reporting a performance, import, or indexing issue. You can skip this if you're reporting a problem you found in our public demo, or if it's a completely unrelated issue, such as incorrect page layout.
@ -82,9 +77,9 @@ This is especially important if you are reporting a performance, import, or inde
*Always provide device, memory, and storage details if you have a backend, performance, import, or indexing issue.*
### 7. Do you use a Reverse Proxy, Firewall, VPN, or CDN?
#### 8. Do you use a Reverse Proxy, Firewall, VPN, or CDN?
Describe your network setup. If applicable, include details about your NGINX or other reverse proxy configuration.
If yes, please specify type and version. You can skip this if you are reporting a completely unrelated issue.
*Always provide this information when you have a reliability, performance, or frontend problem, such as failed uploads, connection errors, broken thumbnails, or video playback issues.*

View file

@ -1,21 +1,29 @@
name: Bug Report 🐞
description: Report a new and clearly identified bug that must be fixed directly in the application.
title: "Bug: Edit the title before submitting"
type: Bug
title: "Category: Short bug description (PLEASE CHANGE)"
labels:
- bug
assignees: []
body:
- type: markdown
attributes:
value: |
Please proceed only if you are sure that this is not a technical support incident and/or a problem with some other software you are using:
1. Thoroughly review our [Getting Started](https://docs.photoprism.app/getting-started/) and [User Guides](https://docs.photoprism.app/user-guide/).
PLEASE ONLY PROCEED IF YOU ARE SURE THIS IS NOT A SUPPORT REQUEST, AND THE ISSUE IS NOT RELATED TO A CUSTOM SETUP OR OTHER SOFTWARE YOU ARE USING:
1. Thoroughly review our [Getting Started](https://docs.photoprism.app/getting-started/) and [User Guides](https://docs.photoprism.app/user-guide/).
2. Work through the [Troubleshooting Checklists](https://docs.photoprism.app/getting-started/troubleshooting/) we provide.
3. Do not report [known issues](https://docs.photoprism.app/known-issues/) or [missing features](https://github.com/photoprism/photoprism/issues) as bugs.
4. Use [GitHub Discussions](https://github.com/photoprism/photoprism/discussions) for community help, and visit our [Support Guide](https://www.photoprism.app/kb/getting-support/) to learn more about support options.
- type: checkboxes
id: prerequisites
attributes:
label: Before You Continue
description: Confirm that you reviewed the guidance above.
options:
- label: This is a new, confirmed bug that has not yet been reported or documented
required: true
- type: textarea
id: documented-behavior
attributes:
label: What is not working as documented?
label: What Is Not Working as Documented?
description: Be specific and include links to the relevant documentation when possible.
placeholder: Describe the incorrect behavior and link to the related documentation on https://docs.photoprism.app/.
validations:
@ -23,65 +31,62 @@ body:
- type: textarea
id: reproduction-steps
attributes:
label: How can we reproduce it?
label: How Can We Reproduce It?
description: Provide numbered steps so we can reproduce the behavior reliably.
placeholder: |
1. Go to ...
2. Click ...
3. Scroll to ...
4. Expected vs. actual result
render: markdown
validations:
required: true
- type: checkboxes
id: prerequisites
attributes:
label: Have you verified that no similar reports exist?
description: Confirm that you read the relevant documentation, this is not a support request, and that no similar reports already exist.
options:
- label: This is a new bug that has not yet been reported or documented
required: true
- type: textarea
id: expected-behavior
attributes:
label: What behavior do you expect?
label: What Behavior Do You Expect?
description: Share the correct outcome you expected to see.
validations:
required: true
- type: textarea
id: possible-cause
attributes:
label: What could be the cause?
label: What Could Be the Cause?
description: Summarize any investigation you have already completed and the potential root cause.
- type: textarea
id: supporting-material
attributes:
label: Logs, Sample Files, or Screenshots
description: Attach or link to files that help us reproduce the problem. Import and indexing issues require sample files and logs.
placeholder: Provide links to archives, logs, or screenshots. See https://www.photoprism.app/contact for secure sharing options.
- type: textarea
id: software-versions
attributes:
label: Which software versions do you use?
label: Which Software Versions Do You Use?
description: Include all relevant software versions so we can reproduce the environment.
value: |
- PhotoPrism Edition & Version (Build):
- Database Type & Version(s):
- Operating System(s):
- Browser Type & Version(s):
- Ad Blockers, Antivirus, and Plugins:
placeholder: |
- PhotoPrism Architecture & Build Number (AMD64, ARM64, ARMv7, ...):
- Database Type & Version (MariaDB, MySQL, SQLite, ...):
- Operating System Types & Versions (Linux, Windows, Android, ...):
- Browser Types & Versions (Firefox, Chrome, Safari on iPhone, ...):
- Ad Blockers, Browser Plugins, or Firewall Software:
render: markdown
validations:
required: true
- type: textarea
id: installation-device
attributes:
label: On what device is PhotoPrism installed?
label: On What Device Is PhotoPrism Installed?
description: Provide hardware details, especially for performance, import, or indexing issues.
value: |
- CPU/Device Type(s):
- Physical Memory & Swap in GB:
- Storage (HDD/SSD/RAID, USB, Network):
placeholder: |
- Device / Processor Type (Raspberry Pi 4, Intel Core i7-3770, AMD Ryzen 7 3800X, ...):
- Physical Memory & Swap Space (GB):
- Storage Type (HDD, SSD, RAID, USB, Network Storage, ...):
- Additional Context:
render: markdown
- type: textarea
id: networking-setup
attributes:
label: Do you use a reverse proxy, firewall, VPN, or CDN?
description: Describe your network setup. If applicable, include details about your NGINX or other reverse proxy configuration.
label: Do You Use a Reverse Proxy, Firewall, VPN, or CDN?
description: Specify type and version if applicable; include NGINX configuration when relevant.
placeholder: Describe proxies, VPNs, CDNs, or firewall software involved. Include versions and configuration snippets when helpful.
- type: upload
id: supporting-material
attributes:
label: Logs, Sample Files, or Screenshots
description: Attach or link to files that help us reproduce the problem. Import and indexing issues require sample files and logs.

View file

@ -4,5 +4,5 @@ contact_links:
url: https://github.com/photoprism/photoprism/discussions
about: Use Discussions for general questions, community support, and troubleshooting help.
- name: Read Our Support Guide
url: https://www.photoprism.app/kb/getting-support/
url: https://www.photoprism.app/kb/getting-support
about: Learn about official support options and how to get assistance with PhotoPrism.

View file

@ -1,57 +1,33 @@
---
name: Feature Request
about: Suggest a new feature or enhancement
title: 'Feature: Edit the title before submitting'
type: Feature
title: 'Category: Short Description (PLEASE CHANGE)'
labels: idea
assignees: ''
---
<details>
PLEASE PROCEED ONLY IF YOU ARE SURE THAT THIS IS NOT A TECHNICAL SUPPORT INCIDENT AND/OR POSSIBLY A PROBLEM WITH SOME OTHER SOFTWARE YOU ARE USING. CHECK OUR PUBLIC ROADMAP AND TRY TO FIND EXISTING FEATURE REQUESTS FIRST:
PLEASE ONLY PROCEED IF YOU ARE ABSOLUTELY SURE THAT THIS IS NOT A TECHNICAL SUPPORT INCIDENT AND/OR A PROBLEM WITH OTHER SOFTWARE YOU ARE USING. PLEASE ALSO CHECK OUR PUBLIC ROADMAP AND TRY TO FIND EXISTING FEATURE REQUESTS FIRST:
- https://link.photoprism.app/roadmap
- https://github.com/photoprism/photoprism/issues
- <https://link.photoprism.app/roadmap>
- <https://github.com/photoprism/photoprism/issues>
VISIT <https://www.photoprism.app/kb/getting-support> TO LEARN MORE ABOUT OUR SUPPORT OPTIONS. THANK YOU FOR YOUR CAREFUL CONSIDERATION!
THANK YOU! 💎
</details>
===============================================================================
**As a <role>, I want <goal>, so that <outcome>.**
All issue descriptions MUST begin with a one-sentence user story in the format shown above:
- Issue titles MUST be concise, use the imperative mood, and start with a single capitalized prefix followed by a colon and a space, e.g. `Search: Add filter for RAW image formats`.
- Follow the user story with information about the problem the feature solves, the alternatives you considered, and any other relevant details.
- Use level-3 Markdown headings to separate sections like `### Background`, `### Additional Context`, `### Open Questions`, and `### Acceptance Criteria`.
### Background
**What problem does this solve, and why would it be valuable to many users?**
**Describe what problem this solves and why this would be valuable to many users**
A clear and concise description of what the problem is and why it is important to solve it.
**What solution do you propose?**
**Describe the solution you'd like**
A clear and concise description of what you suggest to happen.
**Which alternatives or workarounds have you considered?**
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
### Additional Context
**Additional context**
- Add any other context or screenshots about the feature request here.
### Open Questions
- [ ] <question> — options A/B/C and implications.
- [ ] <question> — potential ambiguity or conflict to clarify.
### Acceptance Criteria
- [ ] <component> MUST <expected behavior>
- [ ] <component> SHOULD <expected behavior>
- [ ] <component> MAY <expected behavior>
### References
- Add links to [external documentation](https://www.example.com/), references, or examples.
Add any other context or screenshots about the feature request here.

View file

@ -1,38 +1,34 @@
name: Feature Request
description: Suggest a new feature or enhancement.
title: "Feature: Edit the title before submitting"
type: Feature
title: "Category: Short feature description (PLEASE CHANGE)"
labels:
- idea
assignees: []
body:
- type: markdown
attributes:
value: |
Before submitting a new request, please check our GitHub roadmap and issues for similar requests:
- https://link.photoprism.app/roadmap
- https://github.com/photoprism/photoprism/issues
- type: textarea
id: user-story
Be sure to [check our roadmap](https://link.photoprism.app/roadmap) and [search GitHub for similar features](https://github.com/photoprism/photoprism/issues) before submitting a new request.
- type: checkboxes
id: request-prep
attributes:
label: User Story
description: |
Feature requests MUST begin with a one sentence user story.
**As a <role>, I want <goal>, so that <outcome>.**
placeholder: "**As a <role>, I want <goal>, so that <outcome>.**"
value: "**As a <role>, I want <goal>, so that <outcome>.**"
validations:
required: true
label: Confirmation
description: Make sure you completed these steps before opening a new request.
options:
- label: I checked this request against the roadmap and existing issues
required: true
- type: textarea
id: problem-statement
attributes:
label: What problem does this solve, and why would it be valuable to many users?
description: Explain the underlying problem and why it is important to solve it.
label: What Problem Does This Solve and Why Is It Valuable?
description: Explain the underlying problem and why solving it matters to many users.
placeholder: Describe the problem, why it is important, and who is affected.
validations:
required: true
- type: textarea
id: proposed-solution
attributes:
label: What solution do you propose?
label: What Solution Would You Like?
description: Provide a clear and concise description of the feature or enhancement you propose.
placeholder: Outline the proposed solution or experience from the user's perspective.
validations:
@ -40,34 +36,12 @@ body:
- type: textarea
id: alternatives
attributes:
label: Which alternatives or workarounds have you considered?
description: Describe alternative solutions, workarounds, or related features you evaluated.
label: What Alternatives Have You Considered?
description: Describe other options you explored and why they are less suitable.
placeholder: Mention any alternative solutions, workarounds, or related features you evaluated.
- type: checkboxes
id: prerequisites
attributes:
label: Have you verified that no similar issues exist?
options:
- label: This is not a support request, and I verified that no similar issues exist
required: true
- type: textarea
id: additional-context
attributes:
label: Additional Context
description: Add any other context or screenshots about the feature request here.
placeholder: Add any other context or screenshots that help explain the request.
- type: textarea
id: acceptance-criteria
attributes:
label: Acceptance Criteria
description: Use checklist items and RFC 2119 keywords such as MUST, SHOULD, and MAY to define the expected behavior.
placeholder: |
- [ ] <component> MUST <expected behavior>.
- [ ] <component> SHOULD <expected behavior>.
- [ ] <component> MAY <expected behavior>.
value: |
- [ ] <component> MUST <expected behavior>.
- [ ] <component> SHOULD <expected behavior>.
- [ ] <component> MAY <expected behavior>.
validations:
required: true
description: Share relevant context, references, or screenshots that illustrate the request.
placeholder: Add links, references, or screenshots that help explain the request.

9
.gitignore vendored
View file

@ -28,13 +28,10 @@ __pycache__
venv
.venv
.env*
!.env.example
.tmp
.nv
.eslintcache
.gocache
.gitconfig
.system
/pro/
/portal/
/plus/
@ -52,14 +49,12 @@ venv
/frontend/tests_output/
frontend/coverage/
**/__screenshots__/
/.playwright-mcp/
/photoprism
/photoprism-*
/photos/originals/*
/photos/import/*
/storage/
/build/
/setup/Makefile
/assets/docs/
/assets/facenet/
/assets/nasnet/
@ -74,6 +69,7 @@ frontend/coverage/
*.override.md
/AGENTS_*
/REPOS.md
/CLAUDE.md
/TODO.md
join_token
client_secret
@ -90,10 +86,9 @@ Thumbs.db
.glide
.idea
.codex
.claude
.config
.local
*.local.json
*.local.yaml
.project
.vscode
*.tmproj

View file

@ -239,48 +239,6 @@ debug = false
action = "search"
object = "*"
[[users]]
name = "mona"
givenname = "Mona"
objectClass = "manager"
displayName = "Mona Man"
sn = "Man"
userPrincipalName = "mona@example.com"
mail = "mona@example.com"
uidnumber = 5011
primarygroup = 5511
loginShell = "/bin/bash"
otherGroups = [5505]
passsha256 = "4314c1fe282face45336b1422a3285c5ff31a39c8e24425615fa53a43b718493" # photoprism
[[users.customattributes]]
photoprismRoleManager = ["true"]
photoprismNoLogin = ["false"]
photoprismWebdav = ["true"]
[[users.capabilities]]
action = "search"
object = "*"
[[users]]
name = "laura"
givenname = "Laura"
objectClass = "admin"
displayName = "Laura Scope"
sn = "Doe"
userPrincipalName = "laura@example.com"
mail = "laura@example.com"
uidnumber = 5001
primarygroup = 5501
loginShell = "/bin/bash"
otherGroups = [5501, 5508]
passsha256 = "4314c1fe282face45336b1422a3285c5ff31a39c8e24425615fa53a43b718493" # photoprism
[[users.customattributes]]
photoprismRoleAdmin = ["true"]
photoprismNoLogin = ["false"]
photoprismWebdav = ["true"]
[[users.capabilities]]
action = "search"
object = "*"
[[users]]
name = "max"
givenname = "Max"
@ -340,8 +298,4 @@ debug = false
[[groups]]
name = "PhotoPrism-viewer"
gidnumber = 5510
[[groups]]
name = "PhotoPrism-manager"
gidnumber = 5511
gidnumber = 5510

View file

@ -2,4 +2,5 @@
user=root
password=photoprism
host=mariadb
port=4001
skip-ssl=true

View file

@ -12,13 +12,3 @@ tests/upload-files/
*.html
*.md
.*
# JS / Vue / configs — ESLint owns formatting (see frontend/eslint.config.mjs).
# Prettier on these collapses intentional multi-line method chains and
# predicate lists; we deliberately moved them off Prettier's autofix path.
# Vue templates are formatted by eslint-plugin-vue's flat/recommended preset
# (vue/html-indent, vue/html-quotes, vue/max-attributes-per-line, etc.).
**/*.js
**/*.mjs
**/*.cjs
**/*.vue

View file

@ -1,3 +0,0 @@
.*
*.db
__*

View file

@ -1,17 +0,0 @@
# Privacy defaults: opt out of third-party telemetry, analytics, update checks,
# and other non-essential network calls. Uses plain KEY=VALUE lines so this
# file can be referenced by Docker Compose via env_file: and sourced from
# shells, in addition to being included by the Makefile.
DO_NOT_TRACK=true
DISABLE_AUTOUPDATER=1
DISABLE_TELEMETRY=1
DISABLE_ERROR_REPORTING=1
DISABLE_FEEDBACK_COMMAND=1
NO_UPDATE_NOTIFIER=true
NPM_CONFIG_UPDATE_NOTIFIER=false
NPM_CONFIG_AUDIT=false
NPM_CONFIG_FUND=false
GH_TELEMETRY=false
CLAUDE_CODE_DISABLE_FEEDBACK_SURVEY=1
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1

608
AGENTS.md
View file

@ -1,173 +1,519 @@
# PhotoPrism Repository Guidelines
# PhotoPrism® — Repository Guidelines
**Last Updated:** May 5, 2026
**Last Updated:** March 2, 2026
## Purpose
Entry point for agents and humans.
This file tells automated coding agents (and humans) where to find the single sources of truth for building, testing, and contributing to this repository. Visit https://agents.md/ to learn more.
## Sources of Truth
- Makefile: https://github.com/photoprism/photoprism/blob/develop/Makefile
- Setup guide: https://docs.photoprism.app/developer-guide/setup/
- Test guide: https://docs.photoprism.app/developer-guide/tests/
- Makefile targets (always prefer existing targets): https://github.com/photoprism/photoprism/blob/develop/Makefile
- Developer Guide Setup: https://docs.photoprism.app/developer-guide/setup/
- Developer Guide Tests: https://docs.photoprism.app/developer-guide/tests/
- Contributing: https://github.com/photoprism/photoprism/blob/develop/CONTRIBUTING.md
- Security: https://github.com/photoprism/photoprism/blob/develop/SECURITY.md
- REST API: https://docs.photoprism.dev/ and https://docs.photoprism.app/developer-guide/api/
- Code maps: [`CODEMAP.md`](CODEMAP.md), [`frontend/CODEMAP.md`](frontend/CODEMAP.md)
- Package docs: `README.md` files under `internal/`, `pkg/`, `frontend/`, and `frontend/src/`
- Frontend dependency pins, override layer, and orphan-audit pattern: [`frontend/README.md`](frontend/README.md) (read before bumping any non-caret pin or adding/removing a top-level dep)
- AI/Vision docs: [`internal/ai/face/README.md`](internal/ai/face/README.md), [`internal/ai/vision/README.md`](internal/ai/vision/README.md), [`internal/ai/vision/openai/README.md`](internal/ai/vision/openai/README.md), [`internal/ai/vision/ollama/README.md`](internal/ai/vision/ollama/README.md)
- Glossary: [`GLOSSARY.md`](GLOSSARY.md)
- When dependencies change, regenerate `NOTICE` files with `make notice`; do not edit `NOTICE` or `frontend/NOTICE` manually.
- REST API: https://docs.photoprism.dev/ (Swagger), https://docs.photoprism.app/developer-guide/api/ (Docs)
- Code Maps: [`CODEMAP.md`](CODEMAP.md) (Backend/Go), [`frontend/CODEMAP.md`](frontend/CODEMAP.md) (Frontend/JS)
- Packages: `README.md` files under `internal/`, `pkg/`, and `frontend/src/`, e.g. [`internal/photoprism/README.md`](internal/photoprism/README.md), [`internal/photoprism/batch/README.md`](internal/photoprism/batch/README.md), [`internal/config/README.md`](internal/config/README.md), [`internal/server/README.md`](internal/server/README.md), [`internal/api/README.md`](internal/api/README.md), [`internal/thumb/README.md`](internal/thumb/README.md), [`internal/ffmpeg/README.md`](internal/ffmpeg/README.md), and [`frontend/src/common/README.md`](frontend/src/common/README.md).
- Face Detection & Embeddings: [`internal/ai/face/README.md`](internal/ai/face/README.md)
- Vision Config & Engines: [`internal/ai/vision/README.md`](internal/ai/vision/README.md), [`internal/ai/vision/openai/README.md`](internal/ai/vision/openai/README.md), [`internal/ai/vision/ollama/README.md`](internal/ai/vision/ollama/README.md)
- Terminology Glossary: [`GLOSSARY.md`](GLOSSARY.md) (single source for term definitions across specs/docs)
- Regenerate `NOTICE` files with `make notice` when dependencies change (e.g., updates to `go.mod`, `go.sum`, `package-lock.json`, or other lockfiles). Do not edit `NOTICE` or `frontend/NOTICE` manually.
## Subtree Guides
> 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`.
- [`internal/AGENTS.md`](internal/AGENTS.md): internal Go rules.
- [`internal/api/AGENTS.md`](internal/api/AGENTS.md): API rules.
- [`internal/config/AGENTS.md`](internal/config/AGENTS.md): config rules.
- [`internal/commands/AGENTS.md`](internal/commands/AGENTS.md): CLI rules.
- [`internal/photoprism/AGENTS.md`](internal/photoprism/AGENTS.md): import and index rules.
- [`internal/service/cluster/AGENTS.md`](internal/service/cluster/AGENTS.md): cluster rules.
- [`frontend/AGENTS.md`](frontend/AGENTS.md): frontend rules.
- [`pkg/AGENTS.md`](pkg/AGENTS.md): `pkg/*` security and test rules.
### Local Agent Progress
Optional nested repositories such as `plus/`, `pro/`, `portal/`, and `specs/` may contain their own `AGENTS.md` files. When present, treat those files as additional directory-local guidance.
## Local Agent Progress
- Use `.agents/TODO.md` for actionable tasks and `.agents/DONE.md` for completed work.
- These files are local workflow aids and may not exist in every workspace.
- Use root-level task files to track progress and handoff notes across sessions/environments:
- `AGENTS_TODO.md` for actionable tasks.
- `AGENTS_DONE.md` for completed tasks.
- These files are local workflow aids and may not exist yet in a given workspace.
## Style Notes
### Commit Messages
- Use concise imperative subjects with a one-word prefix, for example `Config: Add tests for "darktable-cli" path detection`.
- Append issue or PR IDs when relevant.
- Commit messages must not exceed 80 characters.
Use concise, imperative subjects with a one-word prefix indicating the scope or topic:
- `Config: Add tests for "darktable-cli" path detection`
If the commit relates to specific issues or pull requests, reference their IDs in the message:
- `Docker: Use two stage build to reduce image size #123 #5632`
Commit messages must not exceed 80 characters in length.
### GitHub Issues
- Titles MUST be concise, imperative, and start with one capitalized prefix plus `: `, for example `Search: Add filter for RAW image formats`.
- Descriptions MUST begin with a one-sentence bold user story: `**As a <role>, I want <goal>, so that <outcome>.**`
- Use level-3 Markdown headings for sections within issue descriptions, for example `### Acceptance Criteria`.
- Follow with behavior, rationale, technical considerations, and constraints.
- End with `- [ ]` checklist items for the acceptance criteria, each using `MUST`, `SHOULD`, or `MAY`.
- Keep the checklist current: once the work for a criterion is implemented **and verified**, mark it done (`- [x]`).
- Leave items that are unverified, not yet implemented, or skipped optional (`MAY`) enhancements unchecked.
- An issue is complete only when every `MUST` is checked; never tick a box on the strength of a plan alone or an unrun test.
- When referencing an issue from a commit that fulfills some of its criteria, update the matching boxes first.
- Agents MUST create, edit, close, reopen, relabel, or otherwise modify GitHub issues only when explicitly requested by the user.
Issue titles MUST be concise, use the imperative mood, and start with a single capitalized prefix followed by a colon and a space, e.g. `Search: Add filter for RAW image formats`.
Issue descriptions MUST begin with a one-sentence **User Story** where the sentence itself is fully bold in the format: `**As a <role>, I want <goal>, so that <outcome>.**`
Follow the User Story with a clear summary of the expected behavior, rationale, technical considerations, and constraints.
Descriptions MUST conclude with a checklist of **Acceptance Criteria**:
- Use GitHub checklist formatting: `- [ ]`
- Criteria MUST be clear, testable, and unambiguous.
- Each item MUST use one of the following priority keywords:
- `MUST` — required for the issue to be considered complete
- `SHOULD` — strongly recommended but not strictly required
- `MAY` — optional enhancement
Additional details MAY be included as needed, such as related issues, references, screenshots, or external resources.
> Agents MUST create, edit, close, reopen, relabel, or otherwise modify GitHub issues only when explicitly requested by the user.
### Specifications & Documentation
- Markdown headings use a Chicago-style title case, with additional code- and path-aware normalization rules (see *Title Case rules* below). Always spell the product name as `PhotoPrism`.
- Put option flags before positional arguments unless the command requires another order.
- Use RFC 3339 UTC timestamps and valid ID, UID, and UUID examples in docs and tests.
- The nested `specs/` repository may be absent. Do not add main-repo `Makefile` targets that depend on it; when present, you may run its tools manually.
- Testing guides live at `specs/dev/backend-testing.md` and `specs/dev/frontend-testing.md`.
- Do not read, analyze, or modify `specs/generated/`; refer humans to `specs/generated/README.md` when regeneration is needed.
- Refresh `**Last Updated:**` when you change document contents, but leave it unchanged for whitespace-only or formatting-only edits.
- Nested Git repositories may appear ignored; change into them before staging or committing updates.
- Document headings must use **Title Case** (in APA or AP style) across Markdown files to keep generated navigation and changelogs consistent. Always spell the product name as `PhotoPrism`; this proper noun is an exception to generic naming rules.
- When writing CLI examples or scripts, place option flags before positional arguments unless the command requires a different order.
- Use RFC 3339 UTC timestamps in request and response examples, and valid ID, UID and UUID examples in docs and tests.
- Technical specifications in the nested `specs/` subrepository may not be present in every clone or environment. Do not add `Makefile` targets in the main project that depend on `specs/` paths. When `specs/` is available, you MAY run its tools manually (e.g., `bash specs/scripts/lint-status.sh`), but the main repo must remain buildable without `specs/`.
- Testing Guides: `specs/dev/backend-testing.md` (Backend/Go), `specs/dev/frontend-testing.md` (Frontend/JS)
- Auto-generated configuration and command references live under `specs/generated/`. Agents MUST NOT read, analyze, or modify anything in this directory; refer humans to `specs/generated/README.md` if regeneration is required.
- Nested Git repositories may appear to be ignored; if so, change directories before staging or committing updates.
Title Case rules (Chicago-style, with code- and path-aware normalization):
- Capitalize the first word, the first word after a colon, dash, or end punctuation, and all major words, including the second part of a hyphenated major word.
- Lowercase only articles, short conjunctions, and short prepositions of three letters or fewer when they are not in one of those positions.
- Preserve known acronyms (for example, API, CLI, HTTP, JSON) and slash-separated acronym groups (for example, CSV/TSV) as uppercase.
- Preserve RFC 2119 / RFC 8174 normative keywords (MUST, SHOULD, MAY, SHALL, REQUIRED, RECOMMENDED, OPTIONAL) as uppercase when used in their normative sense.
- Preserve inline code spans (`` `foo` ``), file paths (e.g. `docs/foo-bar.md`), and slash commands (e.g. `/grill-me`) verbatim; do not recase their contents.
- Use `&` instead of `And`/`Or` in headings.
> **Title Case** rules (APA/AP implementation):
> - Capitalize the first word of a title/heading and the first word of a subtitle.
> - Capitalize the first word after a colon, an em dash, or end punctuation.
> - Capitalize major words, including the second part of hyphenated major words.
> - Capitalize all words of four letters or more.
> - Lowercase only minor words of three letters or fewer (articles, short conjunctions, short prepositions), except when they are in one of the positions above.
> - In headings, prefer `&` where needed; do not use `And` or `Or` in titles.
> Refresh the `**Last Updated:**` date at the top of documents whenever you make changes to their contents, using the format `January 20, 2026` (without time); leave it as-is for simple formatting or whitespace-only edits.
## Safety & Data
- If `git status` shows unexpected changes, assume a human may be editing; ask before using reset-style commands.
- Do not run `git config` at either the global or repository level.
- 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`.
- If `git status` shows unexpected changes, assume a human might be editing; if you think you caused them, ask for permission 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 existing caches, workers, and batching strategies already referenced by the code and `Makefile`.
- Consider CPU and memory impact; only suggest profiling or benchmarks when justified.
- If anything here conflicts with the `Makefile` or the sources of truth, ask for clarification before proceeding.
- 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.
## Project Layout & Shared Rules
> If anything in this file conflicts with the `Makefile` or Sources of Truth, **ask** for clarification before proceeding.
- Backend: Go in `internal/`, `pkg/`, and `cmd/`, backed by MariaDB or SQLite.
- Frontend: Vue 3 plus Vuetify 3 under `frontend/`.
- Local dev and CI use Docker Compose; Traefik provides local TLS via `*.localssl.dev`.
- Code in `pkg/*` must not import from `internal/*`. If you need config, entity, or DB access, add code under `internal/`.
- Shared Go rules:
- After Go edits, run `make fmt-go` and keep `gofmt` tab indentation.
- Every added/modified Go function, including unexported helpers, must have focused test coverage in the corresponding `*_test.go` files; update existing tests or add new ones as needed.
- Every Go package must contain a root `<package>.go` file with the standard license header and a short package description comment.
- Use `pkg/fs` permission constants: `fs.ModeDir`, `fs.ModeFile`, `fs.ModeConfigFile`, `fs.ModeSecretFile`, and `fs.ModeBackupFile`.
- When importing the stdlib `io/fs`, alias it to avoid collisions, for example `iofs "io/fs"` or `gofs "io/fs"`.
- Do not pass stdlib `io/fs` mode flags where permission bits are expected.
- Prefer `filepath.Join` for filesystem paths and `path.Join` only for URL paths.
- Normalize slash-based logical paths stored in DB, config, or API payloads with `clean.SlashPath(...)`.
- Shared JS/Vue rules:
- Added/modified JavaScript functions, including helpers, should be tested whenever practical; update existing tests or add new ones as needed.
- Added/modified Vue components should have component-test coverage, and existing component tests should be updated as needed when behavior changes.
- When adding a metadata source such as `SrcOllama` or `SrcOpenAI`, update both `internal/entity/src.go` and `frontend/src/common/util.js` so backend and UI stay aligned.
## Project Structure & Languages
### JS/Go Code Comments
- Backend: Go (`internal/`, `pkg/`, `cmd/`) + MariaDB/SQLite
- 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/`.
- 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).
- Frontend: Vue 3 + Vuetify 3 (`frontend/`)
- Docker/compose for dev/CI; Traefik is used for local TLS (`*.localssl.dev`)
A doc comment is **required** for every function (including unexported helpers), as well as for every non-trivial Vue `methods:` / `computed:` / watcher:
- Keep comments **compact** and default to one line for "what" in the format `// Name does X.`. Skip trivial getters (`isOpen: () => this.open`).
- Add 1-2 follow-up lines (`// …`) **only** if the "why" is non-obvious: a hidden invariant, a workaround that would otherwise be undone by a future cleanup, a contract a reader can't infer from the code. If readers can infer the "why" from the function body or a nearby line, then omit it.
- Multi-paragraph explanations belong in `specs/`, package `README.md` files, or GitHub issues — never in the source itself.
> Nested Git repositories may appear to be ignored; if so, change directories before staging or committing updates.
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. For short examples in comments, indent code instead of using backticks.
### Web Templates & Shared Assets
Use US English spelling in all code comments (`parameterized`, `behavior`, `color`, `serialize`, `normalize`, `optimize`, …) — not the British `-ised`/`-our`/`-re` variants.
- HTML entrypoints live under `assets/templates/`; key files are `index.gohtml`, `app.gohtml`, `app.js.gohtml`, and `splash.gohtml`. The browser check logic resides in `assets/static/js/browser-check.js` and is included via `app.js.gohtml`; it performs capability checks (Promise, fetch, AbortController, `script.noModule`, etc.) before the main bundle executes.
- To preserve the fallback messaging, keep the script order in `app.js.gohtml` so `browser-check.js` loads before the bundle script (`{{ .config.JsUri }}`). 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 touch `app.js.gohtml` or change how we load the bundle, mirror the update by running commands such as `cd pro && sed -n '1,160p' assets/templates/index.gohtml` (and similarly for `plus` and `portal`) to confirm they include the shared partial instead of hard-coding the bundle tag.
- Splash styles are defined in `frontend/src/css/splash.css`. Add new splash elements (for example `.splash-warning`) there so both public and private editions remain visually consistent.
- Browser baseline: PhotoPrism requires Safari 13 / iOS 13 or current Chrome, Edge, or Firefox. Update the message in `assets/templates/app.js.gohtml` (and the matching CSS) if support changes.
> **Don't include in code comments:** Issue / PR numbers, "previously…" history, alternatives considered, what the function used to do, references to old commits, names of subsequent reviewers, or any narrative that names the change rather than the steady-state behavior. That context belongs in commit messages, specs, or handover notes.
### Frontend Translations
## Agent Runtime
- 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`. Subrepo compatibility targets (`make -C plus gettext-extract`, `make -C pro gettext-extract`, `make -C portal gettext-extract`) delegate to this root target.
- Avoid punctuation-only gettext keys (for example `$gettext("—")`), as they create noisy/unhelpful entries in `frontend/src/locales/translations.pot`.
- Detect container mode by checking for `/.dockerenv`.
- If the repo path is `/go/src/github.com/photoprism/photoprism` and `/.dockerenv` is absent, treat the environment as host mode with a bind mount and prefer host-side Docker commands.
- Bash check: `[ -f "/.dockerenv" ] && echo container || echo host`
- Node.js check: `require("fs").existsSync("/.dockerenv")`
- Inside the container, prefer `npm exec --yes <agent> -- --help` or `npx <agent> ...`; if a global npm install is unavoidable, install it only inside the container.
- The `photoprism/develop` base image and the repo `Makefile` both set `NPM_CONFIG_IGNORE_SCRIPTS=true`, so `npm ci`/`npm install` via `make` targets skip install scripts out of the box. When running npm directly in an environment that does not set or inherit that default, pass `--ignore-scripts` explicitly to mitigate supply-chain attacks. Rebuild native addons with `npm rebuild --ignore-scripts=false <pkg>` — a bare `npm rebuild` is a silent no-op wherever the env default is active.
- On the host, use the vendor-recommended install method and run from the repository root so agent discovery sees this file.
## Agent Runtime (Host vs Container)
## Build, Format & Test
Agents MAY run either:
- Run `make help` to see supported targets.
- Host mode:
- `make docker-build`
- `docker compose up` or `docker compose up -d`
- `docker compose logs -f --tail=100 photoprism`
- `docker compose exec photoprism ./photoprism help`
- `docker compose exec -u "$(id -u):$(id -g)" photoprism <command>` to avoid root-owned files
- `make terminal`
- `docker compose --profile=all down --remove-orphans` or `make down`
- Container mode:
- `make dep`
- `make build-js` and `make build-go`
- `make watch-js` or `cd frontend && npm run watch`
- `./photoprism start`
- Local URLs: `http://localhost:2342/` and, with Traefik, `https://app.localssl.dev/`
- Local compose defaults to `admin` / `photoprism`; inspect `compose.yaml` if they differ.
- Do not use the Docker CLI inside the container; manage Compose from the host instead.
- The public CLI name is always `photoprism`; development-only side-by-side binaries may use edition-specific names.
- Our command examples assume a Linux or Unix shell on 64-bit AMD64 or ARM64; see the Developer Guide FAQ for Windows-specific notes.
- **Inside the Development Environment container** (recommended for least privilege).
- **On the host** (outside Docker), in which case the agent MAY start/stop the Dev Environment as needed.
Formatting and test entry points:
- Full suite: `make test`, `make lint`
- Go-specific lint, format, and package-test rules live in [`internal/AGENTS.md`](internal/AGENTS.md).
- Frontend lint, Vitest, acceptance, and Playwright rules live in [`frontend/AGENTS.md`](frontend/AGENTS.md).
- Go tests live next to their sources; use PascalCase `t.Run(...)` names for related subtests. Keep consecutive subtests inside the same `Test*` function back-to-back without blank lines so the cases read as a compact table; reserve blank lines for separating distinct setup blocks.
- Do not run multiple test commands in parallel; suites share fixtures, assets, and database state.
- Prefer focused test runs such as `go test ./path/to/pkg -run Name -count=1` while iterating.
- Use `mariadb -D photoprism` inside the dev shell when you need to inspect MariaDB state directly.
- Run `shellcheck <file>` on edited shell scripts, or use the corresponding `make` target.
### Detecting the environment (agent logic)
### Container Image Builds
Agents SHOULD detect the runtime and choose commands accordingly:
- **Never mix Debian and Ubuntu `apt` repositories in the same image:**
- Don't add a Debian source to an Ubuntu base (or vice versa) to install a single missing package — the transitive deps drift, apt's solver pulls newer libraries from the foreign distro, and other build steps in the same `RUN` (e.g. `install-libheif.sh` running `apt-get install libavcodec-dev`) silently link against the wrong soname.
- Symptoms surface much later as `dlopen: libfoo.so.N: cannot open shared object file` at image runtime, with the binary referencing a soname that exists only in the foreign distro.
- If a package isn't available in the host distro's repos, prefer (a) a same-distro PPA / backports source, (b) a vendor-supplied .deb (e.g. Google Chrome from `dl.google.com`), or (c) a from-source build pinned to a known version.
- **Inside container if** `/.dockerenv` exists (authoritative signal).
- Path hint: when the project path is `/go/src/github.com/photoprism/photoprism` *and* `/.dockerenv` is absent, assume you are on the host with a bind mount; treat it as host mode and prefer host-side Docker commands.
#### Examples
Bash:
```bash
if [ -f "/.dockerenv" ]; then
echo "container"
else
echo "host"
fi
```
Node.js:
```js
const fs = require("fs");
const inContainer = fs.existsSync("/.dockerenv");
console.log(inContainer ? "container" : "host");
```
### Agent installation and invocation
- **Inside container**: Prefer running agents via `npm exec` (no global install), for example:
- `npm exec --yes <agent-binary> -- --help`
- Or use `npx <agent-binary> ...`
- If the agent is distributed via npm and must be global, install inside the container only:
- `npm install -g <agent-npm-package>`
- Replace `<agent-binary>` / `<agent-npm-package>` with the names from the agents official docs.
- **On host**: Use the vendors recommended install for your OS. Ensure your agent runs from the repository root so it can discover `AGENTS.md` and project files.
## Build & Run (local)
- Run `make help` to see common targets (or open the `Makefile`).
- **Host mode** (agent runs on the host; agent MAY manage Docker lifecycle):
- Build local dev image (once): `make docker-build`
- Start services: `docker compose up` (add `-d` to start in the background)
- Follow live app logs: `docker compose logs -f --tail=100 photoprism` (Ctrl+C to stop)
- All services: `docker compose logs -f --tail=100`
- Last 10 minutes only: `docker compose logs -f --since=10m photoprism`
- Plain output (easier to copy): `docker compose logs -f --no-log-prefix --no-color photoprism`
- Execute a single command in the app container: `docker compose exec photoprism <command>`
- Example: `docker compose exec photoprism ./photoprism help`
- Why `./photoprism`? It runs the locally built binary in the project directory.
- Run as non-root to avoid root-owned files on bind mounts:
`docker compose exec -u "$(id -u):$(id -g)" photoprism <command>`
- Durable alternative: set the service user or `PHOTOPRISM_UID`/`PHOTOPRISM_GID` in `compose.yaml`; if you hit issues, run `make fix-permissions`.
- Open a terminal session in the app container: `make terminal`
- Stop everything when done: `docker compose --profile=all down --remove-orphans` (`make down` does the same)
- **Container mode** (agent runs inside the app container):
- Install deps: `make dep`
- Build frontend/backend: `make build-js` and `make build-go`
- Watch frontend changes (auto-rebuild): `make watch-js`
- Or run directly: `cd frontend && npm run watch`
- Tips: refresh the browser to see changes; running the watcher outside the container can be faster on non-Linux hosts; stop with Ctrl+C
- Start the PhotoPrism server: `./photoprism start`
- Open http://localhost:2342/ (HTTP)
- Or https://app.localssl.dev/ (HTTPS via Traefik reverse proxy)
- Only if Traefik is running and the dev compose labels are active
- Labels for `*.localssl.dev` are defined in the dev compose files, e.g. https://github.com/photoprism/photoprism/blob/develop/compose.yaml
- Admin Login: Local compose files set `PHOTOPRISM_ADMIN_USER=admin` and `PHOTOPRISM_ADMIN_PASSWORD=photoprism`; if the credentials differ, inspect `compose.yaml` (or the active environment) for these variables before logging in.
- Do not use the Docker CLI inside the container; starting/stopping services requires host Docker access. If you need to manage compose while inside the dev container, switch to host mode (or ask a human) instead of running `docker compose` there.
Note: Across our public documentation, official images, and in production, the command-line interface (CLI) name is `photoprism`. Other PhotoPrism binary names are only used in development builds for side-by-side comparisons of the Community Edition (CE) with PhotoPrism Plus (`photoprism-plus`), PhotoPrism Pro (`photoprism-pro`), and PhotoPrism Portal (`photoprism-portal`).
### Operating Systems & Architectures
- Our guides and command examples generally assume the use of a Linux/Unix shell on a 64-bit AMD64 or ARM64 system.
- For Windows-specifics, see the Developer Guide FAQ: https://docs.photoprism.app/developer-guide/faq/#can-your-development-environment-be-used-under-windows
## Code Style & Lint
- Go: run `make fmt-go swag-fmt` to reformat the backend code + Swagger annotations (see `Makefile` for additional targets)
- 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.
- For short examples inside comments, indent code rather than using backticks; godoc treats indented blocks as preformatted.
- Branding: Always spell the product name as `PhotoPrism`; this proper noun is an exception to generic naming rules.
- 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.
- JS/Vue: use the lint/format scripts in `frontend/package.json` (ESLint + Prettier)
- All added code and tests **must** be formatted according to our standards.
## Tests
- From within the Development Environment:
- Full unit test suite: `make test` (runs backend and frontend tests)
- Test frontend/backend: `make test-js` and `make test-go`
- Linting: `make lint` (all), `make lint-go` (golangci-lint with `.golangci.yml`, prints findings without failing due to `--issues-exit-code 0`), `make lint-js` (ESLint/Prettier)
- Go packages: `go test` (all tests) or `go test -run <name>` (specific tests only)
- Need to inspect the MariaDB data while iterating? Connect directly inside the dev shell with `mariadb -D photoprism` and run SQL without rebuilding Go code.
- Go tests live beside sources: for `path/to/pkg/<file>.go`, add tests in `path/to/pkg/<file>_test.go` (create if missing). For the same function, group related cases as `t.Run(...)` sub-tests (table-driven where helpful) and use **PascalCase** for subtest names (for example, `t.Run("Success", ...)`).
- Frontend unit tests use **Vitest**; see scripts in `frontend/package.json`.
- Vitest watch/coverage: `make vitest-watch` and `make vitest-coverage`
- Acceptance tests: use the `acceptance-*` targets in the `Makefile`
- For one-off checks, run a single TestCafe case by `testID` and keep startup/cleanup in the repo root:
```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; running that target from `frontend/` fails with "No rule to make target".
- Portal proxy URI validation: use the Portal test environment with `NODES=2` and verify both instance routes when changing `PHOTOPRISM_PORTAL_PROXY_URI` (Portal) and matching node `PHOTOPRISM_SITE_URL` prefixes; use `PORTAL_TEST_ENV_ARGS=--proxy-uri=/instance/` to regenerate consistent `.env` values.
- Portal test environment default: run a full rebuild via `make -C portal test-env NODES=2` before `make -C portal test-start`; avoid `--no-build` partial refreshes unless you intentionally validate env-only changes, as mixed/stale staged assets can load the wrong frontend edition.
### Playwright MCP Usage
- **Endpoint & Navigation** — Playwright MCP is preconfigured to reach the dev server at `http://localhost:2342/`.
Use `playwright__browser_navigate` to open the login route under the configured frontend URI (default `/library/login` for CE/Plus/Pro, `/portal/admin/login` for Portal), sign in, and then call `playwright__browser_take_screenshot` to capture the page state.
- **Viewport Defaults** — Desktop sessions open with a `1280×900` viewport by default.
Use `playwright__browser_resize` if the viewport is not preconfigured or you need to adjust it mid-run.
- **Mobile Workflows** — When testing responsive layouts, use the `playwright_mobile` server (for example, `playwright_mobile__browser_navigate`).
It launches with a `375×667` viewport, matching a typical smartphone display, so you can capture mobile layouts without manual resizing.
- **Authentication** — Default admin credentials are `admin` / `photoprism`:
- If login fails, check your active Compose file or container environment for `PHOTOPRISM_ADMIN_USER` and `PHOTOPRISM_ADMIN_PASSWORD`.
- Tip: if your MCP supports it, persist a storage state after login and reuse it in later steps to skip re-authentication.
- **Sidebar Navigation** — The sidebar nests items such as `Library → Errors`:
- Expand a parent entry by clicking its chevron before selecting links inside.
- **Session Cleanup** — After scripted interactions, close the browser tab with `playwright__browser_close` (or `playwright_mobile__browser_close`) to keep the MCP session tidy for subsequent runs.
- **Stability / Waiting** — Prefer robust waits over sleeps:
- After navigation: `waitUntil: 'networkidle'` (or wait for a key locator).
- Before clicking: ensure the locator is `visible` and `enabled`.
- Use role/label/text selectors over brittle XPaths.
- **Screenshot Format & Size** — Keep artifacts small and reproducible:
- Prefer **JPEG** with quality (e.g., `quality: 80`) instead of PNG.
- Limit to the visible viewport (`fullPage: false`), unless explicitly required.
- Name files deterministically, e.g., `.local/screenshots/<case>/<step>__<viewport>.jpg` (create the folder if it doesnt exist).
- Avoid embedding large screenshots in chat history—reference the file path instead.
- **Desktop example** (if your MCP tool exposes Playwright options 1:1):
```json
{
"path": ".local/screenshots/fix-event-leaks/login__desktop.jpg",
"type": "jpeg",
"quality": 80,
"fullPage": false
}
```
- **Non-interactive runs** — If `npx` is fetching the MCP server at runtime, add `--yes` to its args (or preinstall and use `--no-install`) to avoid prompts in CI.
### FFmpeg Tests & Hardware Gating
- By default, do not run GPU/HW encoder integrations in CI. Gate with `PHOTOPRISM_FFMPEG_ENCODER` (one of: `vaapi`, `intel`, `nvidia`).
- Negative-path tests should remain fast and always run:
- Missing ffmpeg binary → immediate exec error.
- Unwritable destination → command fails without creating files.
- Prefer command-string assertions when hardware is unavailable; enable HW runs locally only when a device is configured.
### Fast, Focused Test Recipes
- Filesystem + 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`
### CLI Testing Gotchas (Go)
- Exit codes and `os.Exit`:
- `urfave/cli` calls `os.Exit(code)` when a command returns `cli.Exit(...)`, which will terminate `go test` abruptly (often after logs like `http 401:`).
- Use the test helper `RunWithTestContext` (in `internal/commands/commands_test.go`) which temporarily overrides `cli.OsExiter` so the process doesnt exit; you still receive the error to assert `ExitCoder`.
- If you only need to assert the exit code and dont need printed output, you can invoke `cmd.Action(ctx)` directly and check `err.(cli.ExitCoder).ExitCode()`.
- Noninteractive mode: set `PHOTOPRISM_CLI=noninteractive` and/or pass `--yes` to avoid prompts that block tests and CI.
- SQLite DSN in tests:
- `config.NewTestConfig("<pkg>")` defaults to SQLite with a persuite DSN like `.<pkg>.db`. Dont assert an empty DSN for SQLite.
- Clean up any persuite SQLite files in tests with `t.Cleanup(func(){ _ = os.Remove(dsn) })` if you capture the DSN.
### 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 Vuetifys 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 (for example, combobox suggestion lists), ensure they work with the global trap; see the README for troubleshooting tips.
### Filesystem Permissions & io/fs Aliasing (Go)
- Always use our shared permission variables from `pkg/fs` when creating files/directories:
- Directories: `fs.ModeDir` (0o755 with umask)
- Regular files: `fs.ModeFile` (0o644 with umask)
- 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"`.
- Our package is `github.com/photoprism/photoprism/pkg/fs` and provides the only approved permission constants for `os.MkdirAll`, `os.WriteFile`, `os.OpenFile`, and `os.Chmod`.
- Prefer `filepath.Join` for filesystem paths; reserve `path.Join` for URL paths.
- For slash-based logical paths stored in DB/config/API payloads (for example folder album paths), normalize with `clean.SlashPath(...)` instead of repeating ad-hoc `strings.ReplaceAll(..., "\\", "/")` + trim logic.
### 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.
- Where this lives:
- App-level helpers: `internal/photoprism/mediafile.go` (`MediaFile.Copy/Move`).
- Reusable utils: `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.
### 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.
- Where this lives: `pkg/fs/zip.go` (`Unzip`, `UnzipFile`, `safeJoin`).
- Tests to keep:
- Absolute/volume paths rejected (Windows-specific backslash path covered on Windows).
- `..` traversal skipped; `__MACOSX` skipped.
- Per-file and total size limits enforced; directory entries created; nested paths extracted safely.
### HTTP Download — Security Checklist
- Use the shared safe HTTP helper instead of adhoc `net/http` code:
- Package: `pkg/http/safe``safe.Download(destPath, url, *safe.Options)`.
- Default policy in this repo: 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/linklocal ranges.
- All redirect targets are validated; the final connected peer IP is also checked.
- Prefer an imagefocused `Accept` header for image downloads: `"image/jpeg, image/png, */*;q=0.1"`.
- Avatars and small images: use the thin wrapper in `internal/thumb/avatar.SafeDownload` which applies stricter defaults (15s timeout, 10 MiB, `AllowPrivate=false`).
- Tests using `httptest.Server` on 127.0.0.1 must pass `AllowPrivate=true` explicitly to succeed.
- Keep perresource size budgets small; rely on `io.LimitReader` + `Content-Length` prechecks.
## Agent Quick Tips (Do This)
### Testing & Fixtures
- 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, and name each subtest with a PascalCase string.
- Keep Go scratch work inside `internal/...`; Go refuses to import `internal/` packages from directories like `/tmp`, so create temporary helpers under a throwaway folder such as `internal/tmp/` instead of using external paths.
- Prefer focused `go test` runs for speed (`go test ./internal/<pkg> -run <Name> -count=1`, `go test ./internal/commands -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.
- For CLI-driven tests, wrap commands with `RunWithTestContext(cmd, args)` so `urfave/cli` cannot exit the process, and assert CLI output with `assert.Contains`/regex because `show` reports quote strings.
- In `internal/photoprism` tests, rely on `photoprism.Config()` for runtime-accurate behavior; only build a new config if you replace it via `photoprism.SetConfig`.
- 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 creating or editing shell scripts, run `shellcheck <file>` (or the relevant `make` target) and resolve warnings before exiting the task.
- When adding persistent fixtures (photos, files, labels, etc.), always obtain new IDs via `rnd.GenerateUID(...)` with the matching prefix (`entity.PhotoUID`, `entity.FileUID`, `entity.LabelUID`, …) instead of inventing manual strings so the search helpers recognize them.
- For database updates, prefer the `entity.Values` type alias over raw `map[string]interface{}` so helpers stay type-safe and consistent with existing code.
- Reach for `config.NewMinimalTestConfig(t.TempDir())` when a test only needs filesystem/config scaffolding, and use `config.NewMinimalTestConfigWithDb("<name>", t.TempDir())` when you need a fresh SQLite schema without the cached fixture snapshot.
- Config test helpers now auto-discover the repo `assets/` directory; you should not set `PHOTOPRISM_ASSETS_PATH` manually in package `init()` functions unless you have a non-standard layout.
- 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: it shares a singleton instance that runs `InitializeTestData()` and wipes `storage/testdata`. Tests that write to Originals/Import (e.g. WebDAV helpers) should instead call `config.NewMinimalTestConfig(t.TempDir())` (or the DB variant) and follow up with `conf.CreateDirectories()` so they operate on an isolated sandbox.
- Shared fixtures live under `storage/testdata`; `NewTestConfig("<pkg>")` already calls `InitializeTestData()`, but call `c.InitializeTestData()` (and optionally `c.AssertTestData(t)`) when you construct custom configs so originals/import/cache/temp exist. `InitializeTestData()` clears old data, downloads fixtures if needed, then calls `CreateDirectories()`.
- `PhotoFixtures.Get()` and similar helpers return value copies; when a test needs the database-backed row (with associations preloaded), re-query by UID/ID using helpers like `entity.FindPhoto(fixture)` so updates observe persisted IDs and in-memory caches stay coherent.
- For slimmer tests that only need config objects, prefer the new helpers in `internal/config/test.go`: `NewMinimalTestConfig(t.TempDir())` when no database is needed, or `NewMinimalTestConfigWithDb("<pkg>", t.TempDir())` to spin up an isolated SQLite schema without seeding all fixtures.
- When you need illustrative credentials (join tokens, client IDs/secrets, etc.), reuse the shared `Example*` constants (see `internal/service/cluster/examples.go`) so tests, docs, and examples stay consistent.
- Hidden error UI checks for the hidden route under the frontend URI (default `/library/hidden` for CE/Plus/Pro, `/portal/admin/hidden` for Portal) 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 in Hidden results.
### 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`; `acl.ParseRole` already handles `0/false/nil` as none for users.
- 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`) instead of hand-written parsing.
### Import/Index
- ImportWorker may skip files if an identical file already exists (duplicate detection). Use unique copies or assert DB rows after ensuring a nonduplicate destination.
- Mixed roots: when testing related files, keep `ExamplesPath()/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.
- Folder albums use path-first lookup/update (`album_path`) to avoid slug collisions for emoji child paths; re-indexing can repair stale collision titles when a child folder incorrectly shows the parent name, while preserving user-custom titles.
### CLI Usage & Assertions
- Prefer the shared helpers like `DryRunFlag(...)` and `YesFlag()` when adding new CLI flags so behaviour stays consistent across commands.
- Wrap CLI tests in `RunWithTestContext(cmd, args)` so `urfave/cli` cannot exit the process; assert quoted `show` output with `assert.Contains`/regex for the trailing ", or <last>" rule.
- Prefer `--json` responses for automation. `photoprism show commands --json [--nested]` exposes the tree view (add `--all` for hidden entries).
- Use `internal/commands/catalog` to inspect commands/flags without running the binary; when validating large JSON docs, marshal DTOs via `catalog.BuildFlat/BuildNode` instead of parsing CLI stdout.
- Expect `show` commands to return arrays of snake_case rows, except `photoprism show config`, which yields `{ sections: [...] }`, and the `config-options`/`config-yaml` variants, which flatten to a top-level array.
### 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` by setting `c.options.OptionsYaml` before persisting. Use `CliTestContext` in `internal/config/test.go` to exercise new flags.
- For `options.yml` writes in Go code, prefer config-owned persistence helpers over ad-hoc YAML handling: 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 (the helper also covers other paired extensions such as `.toml`/`.tml`).
- When touching configuration in Go code, use the public accessors on `*config.Config` (e.g. `Config.JWKSUrl()`, `Config.SetJWKSUrl()`, `Config.ClusterUUID()`) 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`) so UI badges and server priorities stay aligned.
- 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.
- 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`, or other constants defined there (no additional segments afterwards).
- 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.
- Cluster registry tests (`internal/service/cluster/registry`) currently rely on a full test config because they persist `entity.Client` rows. They run migrations and seed the SQLite DB, so they are intentionally slow. If you refactor them, consider sharing a single `config.TestConfig()` across subtests or building a lightweight schema harness; do not swap to the minimal config helper unless the tests stop touching the database.
- Favor explicit CLI flags: check `c.cliCtx.IsSet("<flag>")` before overriding user-supplied values, and follow the `ClusterUUID` pattern (`options.yml` → CLI/env → generated UUIDv4 persisted).
- 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, and register routes in `internal/server/routes.go`. For new list endpoints default `count=100` (max 1000) and `offset≥0`, document parameters explicitly, and set portal mode via `PHOTOPRISM_NODE_ROLE=portal` plus `PHOTOPRISM_JOIN_TOKEN` when needed.
- Swagger & docs: annotate only routed handlers in `internal/api/*.go`, use full `/api/v1/...` paths, skip helpers, and regenerate docs with `make fmt-go swag-fmt swag` or `make swag-json` (which also strips duplicate `time.Duration` enums). When iterating, target packages with `go test ./internal/api -run Cluster -count=1` or similarly scoped runs.
- 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)`, and prefer OAuth client tokens over non-admin fixtures for negative permission checks.
- Registry data and secrets: store portal/node registry files under `conf.PortalConfigPath()/nodes/` with mode `0600`, keep secrets out of logs, and only return them on creation/rotation flows.
### Formatting (Go)
- Go is formatted by `gofmt` and uses tabs. Do not hand-format indentation.
- Always run after edits: `make fmt-go` (gofmt + goimports).
### API Shape Checklist
- When renaming or adding fields:
- Update DTOs in `internal/service/cluster/response.go` and any mappers.
- Update handlers and regenerate Swagger: `make fmt-go swag-fmt swag`.
- Update tests (search/replace old field names) and examples in `specs/`.
- Quick grep: `rg -n 'oldField|newField' -S` across code, tests, and specs.
### API/CLI Tests: Known Pitfalls
- Gin routes: Register `CreateSession(router)` once per test router; reusing it twice panics on duplicate route.
- CLI commands: Some commands defer `conf.Shutdown()` or emit signals that close the DB. The harness reopens DB before each run, but avoid invoking `start` or emitting signals in unit tests.
- Signals: `internal/commands/start.go` waits on `process.Signal`; calling `process.Shutdown()/Restart()` can close DB. Prefer not to trigger signals in tests.
### 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 (fast feedback)
- 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
- In tests: 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 and metadata
- Pipe method: PhotoPrism remux (ffmpeg) always embeds title/description/created.
- File method: ytdlp writes files; we pass `--postprocessor-args 'ffmpeg:-metadata creation_time=<RFC3339>'` so imports get `Created` even without local remux (fallback from `upload_date`/`release_date`).
- Default remux policy: `auto`; use `always` for the most complete metadata (chapters, extended tags).
- CLI defaults: `photoprism dl` now defaults to `--method pipe` and `--impersonate firefox`; pass `-i none` to disable impersonation. Pipe mode streams raw media and PhotoPrism handles the final FFmpeg remux so metadata (title, description, author, creation time) still comes from `RemuxOptionsFromInfo`.
- Testing workflow: lean on the focused commands above; if importer dedupe kicks in, vary bytes with `YTDLP_DUMMY_CONTENT` or adjust `dest`, and remember `internal/photoprism` is heavy so validate downstream packages first.
### Sessions & Redaction (building sessions in tests)
- Admin session (full view): `AuthenticateAdmin(app, router)`.
- User session: Create a nonadmin 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 dont. `SiteUrl` is safe to show to all roles. Client config also includes `storageNamespace` (SHA-256 of `SiteUrl`) for browser storage scoping and is safe to expose.
### Preflight Checklist
- `go build ./...`
- `make fmt-go swag-fmt swag`
- `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.
### 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 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.
- Config init order: load `options.yml` (`c.initSettings()`), run `EarlyExt().InitEarly(c)`, connect/register the DB, then invoke `Ext().Init(c)`.
- 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.
- Registry & DTOs: use the client-backed registry (`NewClientRegistryWithConfig`)—the file-backed version is legacy—and treat migration as complete only after swapping callsites, building, and running focused API/CLI tests. 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`, and DTOs normalize `Database.{Name,User,Driver,RotatedAt}` while exposing `ClientSecret` only during creation/rotation. `nodes rm --all-ids` cleans duplicate client rows, admin responses may include `AdvertiseUrl`/`Database`, client/user sessions stay redacted, registry files live under `conf.PortalConfigPath()/nodes/` (mode 0600), and `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_` but may be overridden via the portal-only `database-provision-prefix` flag); `BuildDSN` accepts a `driver` but falls back to MySQL format with a warning when unsupported.
- If we add Postgres provisioning support, extend `BuildDSN` and `provisioner.DatabaseDriver` handling, add validations, and return `driver=postgres` consistently in API/CLI.
- Testing: exercise Portal endpoints with `httptest`, guard extraction paths with `pkg/fs.Unzip` size caps, and expect admin-only fields to disappear when authenticated as a client/user session.

View file

@ -1,6 +1,6 @@
PhotoPrism — Backend CODEMAP
**Last Updated:** May 5, 2026
**Last Updated:** February 25, 2026
Purpose
- Give agents and contributors a fast, reliable map of where things live and how they fit together, so you can add features, fix bugs, and write tests without spelunking.
@ -35,24 +35,17 @@ High-Level Package Map (Go)
- `internal/server` — HTTP server, middleware, routing, static/ui/webdav
- `internal/config` — configuration, flags/env/options, client config, DB init/migrate
- `internal/entity` — GORM v1 models, queries, search helpers, migrations
- Label lookup helpers now live in `internal/entity/label*.go`; reuse `FindLabels(...)`, `FindLabelIDs(...)`, and `LabelSlugs(...)` for homophone-aware exact-name/slug resolution instead of duplicating slug SQL in callers.
- `internal/photoprism` — core domain logic (indexing, import, faces, thumbnails, cleanup)
- `internal/ai/vision` — multi-engine computer vision pipeline (models, adapters, schema). Adapter docs: [`internal/ai/vision/openai/README.md`](internal/ai/vision/openai/README.md) and [`internal/ai/vision/ollama/README.md`](internal/ai/vision/ollama/README.md).
- `internal/workers` — background schedulers (index, vision, sync, meta, backup)
- `internal/auth` — ACL, sessions, OIDC
- `internal/service` — cluster/portal, maps, hub, webdav
- WebDAV client docs: `internal/service/webdav/README.md`
- Key WebDAV client behavior:
- Recursive directory discovery prefers `PROPFIND Depth: infinity` and falls back to iterative `Depth: 1` traversal for incompatible servers.
- Hidden dotfiles and entries inside hidden dot-directories are excluded from listings and fallback traversal because they often represent lock files, partial uploads, or provider metadata.
- Service timeouts apply to control operations (`Files`, `Directories`, `Mkdir`, `Delete`), while `Upload` and `Download` avoid total request deadlines and instead use connection-level safeguards.
- `internal/event` — logging, pub/sub, audit; canonical outcome tokens live in `pkg/log/status` (use helpers like `status.Error(err)` when the sanitized message should be the outcome). Docs: `internal/event/README.md`.
- `internal/ffmpeg`, `internal/thumb`, `internal/meta`, `internal/form`, `internal/mutex` — media, thumbs, metadata, forms, coordination. Docs: `internal/ffmpeg/README.md`, `internal/meta/README.md`.
- `pkg/*` — reusable utilities (must never import from `internal/*`), e.g. `pkg/clean`, `pkg/enum`, `pkg/fs`, `pkg/txt`, `pkg/http/header`
Templates & Static Assets
- Entry HTML lives in `assets/templates/index.gohtml`, which includes the splash markup from `app.gohtml` and the SPA loader from `app.js.gohtml`.
- OIDC login completion for the SPA is bridged through `assets/templates/auth.gohtml`, which clears legacy/namespaced session keys and writes the session into the preferred namespaced browser store selected by the login UI toggle in `frontend/src/page/auth/login.vue`.
- The browser check logic resides in `assets/static/js/browser-check.js` and is included via `app.js.gohtml`; it performs capability checks (Promise, fetch, AbortController, `script.noModule`, etc.) before the main bundle runs.
- Update this file (and the partial) in lockstep with `pro/assets/templates/index.gohtml`, `plus/assets/templates/index.gohtml`, and `portal/assets/templates/index.gohtml`, because those editions import the same partial.
- Keep the script tag order unchanged so the browser check executes before the main bundle.
@ -70,7 +63,7 @@ HTTP API
- `make swag-json` runs a stabilization step (`swaggerfix`) removing duplicated enums for `time.Duration`; API uses integer nanoseconds for durations.
- `/api/v1/metrics` (see `internal/api/metrics.go`) exposes Prometheus metrics, including cached filesystem/account usage derived from `config.Usage()`, registered user/guest totals, and portal cluster node counts when `NodeRole=portal`; the handler returns the standard Prometheus exposition content type (`text/plain; version=0.0.4`).
- Common groups in `routes.go`: sessions, OAuth/OIDC, config, users, services, thumbnails, video, downloads/zip, index/import, photos/files/labels/subjects/faces, batch ops, cluster, technical (metrics, status, echo).
- Hidden search behavior (used by the hidden route under the configured frontend URI, default `/library/hidden` for CE/Plus/Pro and `/portal/hidden` for Portal) is implemented in `internal/entity/search/photos.go`:
- Hidden search behavior (used by the hidden route under the configured frontend URI, default `/library/hidden` for CE/Plus/Pro and `/portal/admin/hidden` for Portal) is implemented in `internal/entity/search/photos.go`:
- `frm.Hidden` enforces `photos.photo_quality = -1` and `photos.deleted_at IS NULL`.
- Non-hidden searches exclude errored files by default (`files.file_error = ''`) unless `frm.Error` is explicitly set.
- Search DTOs in `internal/entity/search/photos_results.go` expose `FileError` (`files.file_error`) so clients can render hidden reasons without loading full file details first.
@ -136,6 +129,7 @@ Cluster / Portal
- Registry/provisioner: `internal/service/cluster/registry/*`, `internal/service/cluster/provisioner/*`.
- Theme endpoint (server): GET `/api/v1/cluster/theme`; client/CLI installs theme only if missing or no `app.js`.
- Portal-only extensions: `portal/internal/portal` (Portal defaults, flags, provisioning options, `/i/*` proxy router).
- See specs cheat sheet: `specs/portal/README.md`.
Logging & Events
- Logger and event hub: `internal/event/*`; `event.Log` is the shared logger.
@ -242,8 +236,8 @@ Cluster Registry & Provisioner Cheatsheet
- Registration returns `Secrets.ClientSecret`; the CLI persists it under config `NodeClientSecret`.
- Admin responses may include `AdvertiseUrl` and `Database`; non-admin responses are redacted by default.
- Cluster CLI highlights:
- `photoprism cluster register` supports `--site-url` and `--advertise-url`. Both values are always forwarded to the Portal regardless of whether they differ.
- Automatic MariaDB credential rotation logic lives in `config.ShouldAutoRotateDatabase()` and is shared by both the CLI and node bootstrap.
- `photoprism cluster register` supports `--site-url` and `--advertise-url`. Both values are always forwarded to the Portal; `SiteUrl` no longer depends on being different from the advertised URL.
- Automatic MariaDB credential rotation logic now lives in `config.ShouldAutoRotateDatabase()` and is shared by both the CLI and node bootstrap.
Frequently Touched Files (by topic)
- CLI wiring: `cmd/photoprism/photoprism.go`, `internal/commands/commands.go`
@ -288,6 +282,7 @@ Useful Make Targets (selection)
See Also
- AGENTS.md (repository rules and tips for agents)
- Developer Guide (Setup/Tests/API) — links in AGENTS.md → Sources of Truth
- Specs: `specs/dev/backend-testing.md`, `specs/dev/api-docs-swagger.md`, `specs/portal/README.md`
Go Internal Import Rule
- Keep temporary Go helpers inside `internal/...`; the Go toolchain blocks importing `internal/` packages from directories such as `/tmp`, so use a disposable path like `internal/tmp/` when you need scratch space.

View file

@ -1,8 +1,8 @@
# PhotoPrism® Code of Conduct
**By using the software and services we provide, you agree to our [Terms of Service](https://www.photoprism.app/terms/), including our [Privacy Policy](https://www.photoprism.app/privacy/) and the following Code of Conduct. It explains the "dos and donts" when interacting with our team and other community members.**
**By using the software and services we provide, you agree to our [Terms of Service](https://www.photoprism.app/terms), including our [Privacy Policy](https://www.photoprism.app/privacy) and the following Code of Conduct. It explains the "dos and donts" when interacting with our team and other community members.**
*This Code of Conduct was last updated on May 30, 2026. A German translation is available at [photoprism.app/de/code-of-conduct](https://www.photoprism.app/de/code-of-conduct/).*
*This Code of Conduct was last updated on May 6, 2024. A German translation is available at [photoprism.app/de/code-of-conduct](https://www.photoprism.app/de/code-of-conduct).*
## Rules
@ -30,12 +30,16 @@ We have found that many of the issues that new users get upset about when they r
## Reporting
We encourage all community members to resolve problems on their own whenever possible. Serious and persistent violations, such as disrespectful, abusive, harassing, or otherwise unacceptable behavior, [may be reported](https://www.photoprism.app/contact/) to us.
We encourage all community members to resolve problems on their own whenever possible. Serious and persistent violations, such as disrespectful, abusive, harassing, or otherwise unacceptable behavior, [may be reported](https://www.photoprism.app/contact) to us.
## Enforcement
Our community standards will be enforced as necessary to protect everyone's well-being and ensure our discussion forums, chat rooms, and other infrastructure, such as GitHub, can be used as intended.
Our community standards will be enforced as necessary to protect everyone's well-being and to ensure that our discussion forums, chat rooms, and other infrastructure can be used as intended.
In cases where violations may be unintentional and improvement seems possible, we aim to issue warnings before taking further action.
Initial warnings may be issued in the form of a [snarky comment](https://www.urbandictionary.com/define.php?term=snarky), especially if you seem reckless or [surprisingly harsh](https://github.com/photoprism/photoprism/issues/281#issuecomment-1207233135). In serious cases, we will provide a link to this Code of Conduct to avoid misunderstandings. We also reserve the right to delete rants, personal attacks, spam, and unsolicited advertising from our community forums.
In serious cases, we may use technical measures to restrict your access to our infrastructure, including GitHub, forums, and chats, either temporarily or permanently. We also reserve the right to delete rants, personal attacks, spam, and unsolicited advertisements. If you believe we made a mistake, you may [email us](https://www.photoprism.app/contact/) to request an appeal.
Getting a simple **\*plonk\***[^1] in response finally signals that we have lost hope and you're being ignored according to **Rule &#35;3**. This old tradition from Usenet days is as time-saving as it is clear. It is not meant in a disrespectful way.
In addition, we may use technical measures to temporarily or permanently restrict your access to our infrastructure, including forums and chats.
[^1]: \*plonk\* including variants such as "Plonk." stands for the metaphorical sound of a user hitting the bottom of the kill file. It was first used in [Usenet forums](https://en.everybodywiki.com/Plonk_(Usenet)), a worldwide distributed discussion system and precursor to the Web.

View file

@ -4,9 +4,9 @@ We welcome contributions of any kind, including blog posts, tutorials, testing,
## Join the Community ##
Follow us on [Mastodon](https://floss.social/@photoprism), [Bluesky](https://bsky.app/profile/photoprism.app), or join the [Community Chat](https://link.photoprism.app/chat) to get regular updates, connect with other users, and discuss your ideas. Our [Code of Conduct](https://www.photoprism.app/code-of-conduct/) explains the "dos and donts" when interacting with other community members.
Follow us on [Mastodon](https://floss.social/@photoprism), [Bluesky](https://bsky.app/profile/photoprism.app), or join the [Community Chat](https://link.photoprism.app/chat) to get regular updates, connect with other users, and discuss your ideas. Our [Code of Conduct](https://www.photoprism.app/code-of-conduct) explains the "dos and donts" when interacting with other community members.
As a [contributor](https://docs.photoprism.app/developer-guide/), you are also welcome to [contact us directly](https://www.photoprism.app/contact/) if you have something on your mind that you don't want to discuss publicly. Please note, however, that due to the high volume of emails we receive, our team may be unable to get back to you immediately. We do our best to respond within five business days or less.
As a [contributor](https://docs.photoprism.app/developer-guide/), you are also welcome to [contact us directly](https://www.photoprism.app/contact) if you have something on your mind that you don't want to discuss publicly. Please note, however, that due to the high volume of emails we receive, our team may be unable to get back to you immediately. We do our best to respond within five business days or less.
## Not a Developer? No Problem ##
@ -36,22 +36,20 @@ We kindly ask you not to report bugs via GitHub Issues **unless you are certain
- When reporting a problem, always include the software versions you are using and other information about your environment such as [browser, browser plugins](https://docs.photoprism.app/getting-started/troubleshooting/browsers/), operating system, [storage type](https://docs.photoprism.app/getting-started/troubleshooting/performance/#storage), [memory size](https://docs.photoprism.app/getting-started/troubleshooting/performance/#memory), and [processor](https://docs.photoprism.app/getting-started/troubleshooting/performance/#server-cpu)
- Note that all issue **subscribers receive an email notification** from GitHub whenever a new comment is added, so these should only be used for sharing important information and not for discussions, questions or expressing personal opinions
- [Contact us](https://www.photoprism.app/contact/) or [a community member](https://link.photoprism.app/discussions) if you need help, it could be a local configuration problem, or a misunderstanding in how the software works
- [Contact us](https://www.photoprism.app/contact) or [a community member](https://link.photoprism.app/discussions) if you need help, it could be a local configuration problem, or a misunderstanding in how the software works
- This gives our team the opportunity to [improve the docs](https://docs.photoprism.app/getting-started/troubleshooting/) and provide best-in-class support to you, instead of handling unclear/duplicate bug reports or triggering a flood of notifications by responding to comments
## Submitting Pull Requests ##
Follow our [step-by-step guide](https://docs.photoprism.app/developer-guide/pull-requests) to learn how to submit new features, bug fixes, and documentation enhancements.
You are welcome to use AI tools for research, drafting, or other supporting tasks. However, we do not accept fully AI-generated pull requests at this time. Every contribution must be carefully reviewed, tested, and understood by a human contributor who can discuss the implementation details and maintain the change afterward.
Pull requests solving ["help wanted"](https://github.com/photoprism/photoprism/labels/help%20wanted) issues are the easiest to merge and the most helpful to us, as they allow us to spend more time on core functionality and other issues that are difficult for external contributors to work on. If you are new to this project, anything labeled ["easy"](https://github.com/photoprism/photoprism/labels/easy) may be a good first contribution.
**Be aware that reviewing, testing and finally merging pull requests requires significant resources on our side. It can therefore take several months if it is not just a small fix, especially if extensive testing is needed to prevent bugs from getting into our stable version.**
## Contributor License Agreement (CLA) ##
After you submit your first pull request, you will be asked to accept our Contributor License Agreement (CLA). Visit [photoprism.app/cla](https://www.photoprism.app/cla/) and [photoprism.app/oss/faq](https://www.photoprism.app/oss/faq/#cla) to learn more.
After you submit your first pull request, you will be asked to accept our Contributor License Agreement (CLA). Visit [photoprism.app/cla](https://www.photoprism.app/cla) and [photoprism.app/oss/faq](https://www.photoprism.app/oss/faq#cla) to learn more.
## Thank You to All Current and Past Sponsors 💎 ##
@ -76,4 +74,4 @@ Because many of these apps and tools were originally developed for internal use
----
*PhotoPrism® is a [registered trademark](https://www.photoprism.app/trademark/). By using the software and services we provide, you agree to our [Terms of Service](https://www.photoprism.app/terms/), [Privacy Policy](https://www.photoprism.app/privacy/), and [Code of Conduct](https://www.photoprism.app/code-of-conduct/). Docs are [available](https://link.photoprism.app/github-docs) under the [CC BY-NC-SA 4.0 License](https://creativecommons.org/licenses/by-nc-sa/4.0/); [additional terms](https://github.com/photoprism/photoprism/blob/develop/assets/README.md) may apply.*
*PhotoPrism® is a [registered trademark](https://www.photoprism.app/trademark). By using the software and services we provide, you agree to our [Terms of Service](https://www.photoprism.app/terms), [Privacy Policy](https://www.photoprism.app/privacy), and [Code of Conduct](https://www.photoprism.app/code-of-conduct). Docs are [available](https://link.photoprism.app/github-docs) under the [CC BY-NC-SA 4.0 License](https://creativecommons.org/licenses/by-nc-sa/4.0/); [additional terms](https://github.com/photoprism/photoprism/blob/develop/assets/README.md) may apply.*

View file

@ -1,11 +1,10 @@
# Ubuntu 26.04 LTS (Resolute Raccoon)
FROM photoprism/develop:260709-resolute
# Ubuntu 25.10 (Questing Quokka)
FROM photoprism/develop:260301-questing
# Harden npm usage by default (applies to npm ci / install in dev container)
ENV NPM_CONFIG_IGNORE_SCRIPTS=true
## Alternative Environments:
# FROM photoprism/develop:questing # Ubuntu 25.10 (Questing Quokka)
# FROM photoprism/develop:plucky # Ubuntu 25.04 (Plucky Puffin)
# FROM photoprism/develop:armv7 # ARMv7 (32bit)
# FROM photoprism/develop:oracular # Ubuntu 24.10 (Oracular Oriole)
@ -18,21 +17,11 @@ ENV NPM_CONFIG_IGNORE_SCRIPTS=true
# FROM photoprism/develop:bullseye # Debian 11 (Bullseye)
# FROM photoprism/develop:buster # Debian 10 (Buster)
# Set default working directory. Override WORKING_DIR (e.g. via compose build
# args populated from .env) to match a custom host-side clone layout; the value
# must stay aligned with compose's working_dir and the source bind mount target.
ARG WORKING_DIR=/go/src/github.com/photoprism/photoprism
WORKDIR "${WORKING_DIR}"
# Set default working directory.
WORKDIR "/go/src/github.com/photoprism/photoprism"
# Copy source to image.
COPY . .
# Update scripts in image.
COPY --chown=root:root ./scripts/dist/ /scripts/
# Re-install the dev "mariadb" client config so a custom MARIADB_PORT in .env
# is honored even when the base image was built before the port=<n> line was
# removed (no-op once the next dated base image picks up the new .my.cnf).
COPY --chown=root:root --chmod=644 ./.my.cnf /etc/my.cnf
COPY --chown=root:root /scripts/dist/ /scripts/
RUN sudo /scripts/install-yt-dlp.sh

View file

@ -1,3 +1,3 @@
custom: "https://www.photoprism.app/editions/#compare"
custom: "https://www.photoprism.app/editions#compare"
github: photoprism
patreon: photoprism

View file

@ -4,7 +4,7 @@
### Purpose & Scope
- This is the single source of truth for terminology used across PhotoPrism documentation.
- This is the single source of truth for terminology used across `specs/` and related docs.
- Define terms once here and reference this file instead of redefining the same terms in multiple documents.
- Keep technical/API contract names unchanged where required, even when user-facing wording differs.

View file

@ -625,7 +625,7 @@ corporate design, product and service names, and any other brand features
and elements, whether registered or unregistered („Brand Assets“) — are
proprietary assets owned exclusively by PhotoPrism UG („PhotoPrism“). We
reserve the right to object to any use or misuse in any jurisdiction
worldwide. Visit <https://www.photoprism.app/trademark/> to learn more.
worldwide. Visit <https://www.photoprism.app/trademark> to learn more.
(b) Contributors, licensees, business partners, and other third parties
may never claim ownership of PhotoPrism's Brand Assets or brands confusingly

268
Makefile
View file

@ -1,4 +1,4 @@
# Copyright © 2018 - 2026 PhotoPrism UG. All rights reserved.
# Copyright © 2018 - 2025 PhotoPrism UG. All rights reserved.
#
# Questions? Email us at hello@photoprism.app or visit our website to learn
# more about our team, products and services: https://www.photoprism.app/
@ -6,11 +6,6 @@
export GO111MODULE=on
export NPM_CONFIG_IGNORE_SCRIPTS ?= true
ifneq (,$(wildcard .telemetry))
include .telemetry
export $(shell sed -n 's/^[[:space:]]*\([A-Z_][A-Z0-9_]*\)=.*/\1/p' .telemetry)
endif
-include .semver
-include .env
@ -39,7 +34,6 @@ BUILD_ARCH ?= $(shell ./scripts/dist/arch.sh)
export BUILD_ARCH
JS_BUILD_PATH ?= $(shell realpath "./assets/static/build")
TF_VERSION ?= 2.18.0
WORKING_DIR ?= /go/src/github.com/photoprism/photoprism
# Install parameters.
INSTALL_PATH ?= $(BUILD_PATH)/photoprism-ce_$(BUILD_TAG)-$(shell echo $(BUILD_OS) | tr '[:upper:]' '[:lower:]')-$(BUILD_ARCH)
@ -89,25 +83,23 @@ test-commands: run-test-commands
test-photoprism: run-test-photoprism
test-short: run-test-short
test-mariadb: reset-acceptance run-test-mariadb
acceptance-run-chromium: storage/acceptance acceptance-sqlite-restart-1 wait-1 acceptance-api acceptance-sqlite-stop-1 acceptance-auth-sqlite-restart wait-2 acceptance-auth acceptance-auth-sqlite-stop acceptance-sqlite-restart-3 wait-3 acceptance acceptance-sqlite-stop-3
acceptance-run-chromium-short: storage/acceptance acceptance-auth-sqlite-restart wait-1 acceptance-auth-short acceptance-auth-sqlite-stop acceptance-sqlite-restart-2 wait-2 acceptance-short acceptance-sqlite-stop-2
acceptance-auth-run-chromium: storage/acceptance acceptance-auth-sqlite-restart wait-1 acceptance-auth acceptance-auth-sqlite-stop
acceptance-public-run-chromium: storage/acceptance acceptance-sqlite-restart-1 wait-1 acceptance acceptance-sqlite-stop-1
acceptance-api-run-chromium: storage/acceptance acceptance-sqlite-restart-1 wait-1 acceptance-api acceptance-sqlite-stop-1
acceptance-run-chromium: storage/acceptance acceptance-auth-sqlite-restart wait acceptance-auth acceptance-auth-sqlite-stop acceptance-sqlite-restart wait-2 acceptance acceptance-sqlite-stop
acceptance-run-chromium-short: storage/acceptance acceptance-auth-sqlite-restart wait acceptance-auth-short acceptance-auth-sqlite-stop acceptance-sqlite-restart wait-2 acceptance-short acceptance-sqlite-stop
acceptance-auth-run-chromium: storage/acceptance acceptance-auth-sqlite-restart wait acceptance-auth acceptance-auth-sqlite-stop
acceptance-public-run-chromium: storage/acceptance acceptance-sqlite-restart wait acceptance acceptance-sqlite-stop
help: list
list:
@awk '/^[[:alnum:]]+[^[:space:]]+:/ {printf "%s",substr($$1,1,length($$1)-1); if (match($$0,/#/)) {desc=substr($$0,RSTART+1); sub(/^[[:space:]]+/,"",desc); printf " - %s\n",desc} else printf "\n" }' "$(firstword $(MAKEFILE_LIST))"
wait-%:
wait:
sleep 20
wait-2:
sleep 20
show-rev:
@git rev-parse HEAD
show-build:
@echo "$(BUILD_TAG)"
tag-release:
scripts/tag-release.sh $(EDITION) $(TAG_ARGS)
test-all: test acceptance-run-chromium
fmt: fmt-js fmt-go fmt-swag
format: format-tables fmt-go fmt-swag
format-tables: # Format Markdown tables in README.md, AGENTS.md, and CODEMAP.md files.
@set -eu; \
tmp="$$(mktemp)"; \
@ -129,8 +121,7 @@ devtools: install-go dep-npm
.SILENT: help;
logs:
$(DOCKER_COMPOSE) logs -f
down: docker-down
docker-down:
down:
$(DOCKER_COMPOSE) --profile=all down --remove-orphans
docs: swag
swag: swag-json
@ -165,10 +156,10 @@ notice:
fix-permissions:
$(info Updating filesystem permissions...)
@if [ $(UID) != 0 ]; then\
echo "Running \"chown --preserve-root -Rcf $(UID):$(GID) /go $(WORKING_DIR) /photoprism /opt/photoprism /tmp/photoprism\". Please wait."; \
sudo chown --preserve-root -Rcf $(UID):$(GID) /go $(WORKING_DIR) /photoprism /opt/photoprism /tmp/photoprism || true;\
echo "Running \"chmod --preserve-root -Rcf u+rwX $(WORKING_DIR)/* /photoprism /opt/photoprism /tmp/photoprism\". Please wait.";\
sudo chmod --preserve-root -Rcf u+rwX $(WORKING_DIR)/* /photoprism /opt/photoprism /tmp/photoprism || true;\
echo "Running \"chown --preserve-root -Rcf $(UID):$(GID) /go /photoprism /opt/photoprism /tmp/photoprism\". Please wait."; \
sudo chown --preserve-root -Rcf $(UID):$(GID) /go /photoprism /opt/photoprism /tmp/photoprism || true;\
echo "Running \"chmod --preserve-root -Rcf u+rwX /go/src/github.com/photoprism/* /photoprism /opt/photoprism /tmp/photoprism\". Please wait.";\
sudo chmod --preserve-root -Rcf u+rwX /go/src/github.com/photoprism/photoprism/* /photoprism /opt/photoprism /tmp/photoprism || true;\
echo "Done."; \
else\
echo "Running as root. Nothing to do."; \
@ -181,8 +172,6 @@ gettext-compile:
$(MAKE) -C frontend gettext-compile
gettext-clear-fuzzy:
./scripts/gettext-clear-fuzzy.sh
gettext-lint:
node scripts/gettext-lint.mjs
clean:
rm -f *.log .test*
[ ! -f "$(BINARY_NAME)" ] || rm -f $(BINARY_NAME)
@ -227,7 +216,7 @@ install-onnx:
sudo scripts/dist/install-onnx.sh
install-darktable:
sudo scripts/dist/install-darktable.sh
acceptance-sqlite-restart-%: acceptance-sqlite-stop-%
acceptance-sqlite-restart:
cp -f storage/acceptance/backup.db storage/acceptance/index.db
cp -f storage/acceptance/config-sqlite/settingsBackup.yml storage/acceptance/config-sqlite/settings.yml
rm -rf storage/acceptance/sidecar/2020
@ -238,9 +227,9 @@ acceptance-sqlite-restart-%: acceptance-sqlite-stop-%
rm -rf storage/acceptance/originals/2013
rm -rf storage/acceptance/originals/2017
./photoprism --auth-mode="public" -c "./storage/acceptance/config-sqlite" start -d
acceptance-sqlite-stop-%:
acceptance-sqlite-stop:
./photoprism --auth-mode="public" -c "./storage/acceptance/config-sqlite" stop
acceptance-auth-sqlite-restart: acceptance-auth-sqlite-stop
acceptance-auth-sqlite-restart:
cp -f storage/acceptance/backup.db storage/acceptance/index.db
cp -f storage/acceptance/config-sqlite/settingsBackup.yml storage/acceptance/config-sqlite/settings.yml
./photoprism --auth-mode="password" -c "./storage/acceptance/config-sqlite" start -d
@ -281,9 +270,9 @@ dep-list:
go list -u -m -json all | go-mod-outdated -direct
dep-list-all:
go list -u -m -json all | go-mod-outdated
vuln: audit
audit: audit-frontend audit-backend
audit-frontend: npm-audit
audit-frontend:
$(MAKE) -C frontend audit
audit-backend: dep-vuln
dep-audit: dep-vuln
dep-vuln:
@ -301,53 +290,22 @@ npm-version:
dep-npm:
@echo "Installing NPM package manager..."
@if command -v sudo >/dev/null 2>&1; then \
sudo npm install -g --location=global --ignore-scripts --no-audit --no-fund --no-update-notifier "npm@latest"; \
sudo npm install -g --location=global --no-fund --no-audit "npm@latest"; \
else \
npm install -g --location=global --ignore-scripts --no-audit --no-fund --no-update-notifier "npm@latest"; \
npm install -g --location=global --no-fund --no-audit "npm@latest"; \
fi
dep-js: npm-ci
npm-ci:
$(info Clean install of NPM dependencies...)
npm ci --ignore-scripts --no-audit --no-fund --no-update-notifier
npm-install:
$(info Installing NPM dependencies...)
npm install --save --ignore-scripts --no-audit --no-fund --no-update-notifier
npm-update:
$(info Updating NPM dependencies in package.json and package-lock.json...)
npm update --save --package-lock --ignore-scripts --no-audit --no-fund --no-update-notifier
npm-audit:
npm audit --ignore-scripts --no-fund --no-update-notifier
tools: gh claude codex
codex: dep-codex codex-version codex-skills
dep-js:
npm ci --ignore-scripts --no-update-notifier --no-audit
codex: dep-codex codex-version
codex-version:
@echo "🤖 Installed $$(codex --version)."
dep-codex:
@echo "Installing Codex CLI..."
@[ -n "$(CODEX_HOME)" ] && [ "$(CODEX_HOME)" != "/" ] && install -d -m 700 -- "$(CODEX_HOME)" || true
@if command -v sudo >/dev/null 2>&1; then \
sudo npm install -g --location=global --ignore-scripts --no-audit --no-fund --no-update-notifier "@openai/codex@latest"; \
sudo npm install -g --location=global --no-fund --no-audit "@openai/codex@latest"; \
else \
npm install -g --location=global --ignore-scripts --no-audit --no-fund --no-update-notifier "@openai/codex@latest"; \
fi
skills: agents-skills claude-skills
agents-skills: codex-skills
codex-skills:
@if [ -d "specs/.agents/skills" ]; then \
echo "Linking Codex skills from specs/.agents/skills..."; \
install -d -m 755 -- ".agents/skills"; \
for src in specs/.agents/skills/*/; do \
[ -d "$$src" ] || continue; \
name=$$(basename "$$src"); \
link=".agents/skills/$$name"; \
target="../../specs/.agents/skills/$$name"; \
if [ -L "$$link" ] || [ ! -e "$$link" ]; then \
ln -sfn "$$target" "$$link"; \
else \
echo "WARNING: $$link exists and is not a symlink, skipping"; \
fi; \
done; \
else \
echo "No specs/.agents/skills directory found, skipping."; \
npm install -g --location=global --no-fund --no-audit "@openai/codex@latest"; \
fi
gh: dep-gh gh-version
gh-version:
@ -368,34 +326,27 @@ dep-gh:
echo "ERROR: Could not install gh automatically. See https://cli.github.com/"; \
exit 1; \
fi
claude: claude-skills
@[ -n "$(HOME)" ] && [ "$(HOME)" != "/" ] && install -d -m 755 -- "$(HOME)/.local/bin" || true
@[ -n "$(CLAUDE_CONFIG_DIR)" ] && [ "$(CLAUDE_CONFIG_DIR)" != "/" ] && install -d -m 755 -- "$(CLAUDE_CONFIG_DIR)" || true
@if command -v claude >/dev/null 2>&1; then \
echo "Updating Claude Code..."; \
claude update; \
else \
echo "Installing Claude Code..."; \
curl -fsSL https://claude.ai/install.sh | bash; \
claude:
@echo "Installing Claude Code..."
@[ -n "$(HOME)" ] && [ "$(HOME)" != "/" ] || (echo "ERROR: Unsafe HOME path '$(HOME)'"; exit 1)
@if [ -e "$(HOME)/.cache" ] && [ ! -w "$(HOME)/.cache" ]; then \
echo "Fixing ownership of \"$(HOME)/.cache\"..."; \
if command -v sudo >/dev/null 2>&1; then \
sudo chown "$(UID):$(GID)" "$(HOME)/.cache"; \
else \
chown "$(UID):$(GID)" "$(HOME)/.cache"; \
fi; \
fi
claude-skills:
@if [ -d "specs/.claude/skills" ]; then \
echo "Linking Claude Code skills from specs/.claude/skills..."; \
install -d -m 755 -- ".claude/skills"; \
for src in specs/.claude/skills/*/; do \
[ -d "$$src" ] || continue; \
name=$$(basename "$$src"); \
link=".claude/skills/$$name"; \
target="../../specs/.claude/skills/$$name"; \
if [ -L "$$link" ] || [ ! -e "$$link" ]; then \
ln -sfn "$$target" "$$link"; \
else \
echo "WARNING: $$link exists and is not a symlink, skipping"; \
fi; \
done; \
else \
echo "No specs/.claude/skills directory found, skipping."; \
@if [ -e "$(HOME)/.cache/claude" ] && [ ! -w "$(HOME)/.cache/claude" ]; then \
echo "Fixing ownership of \"$(HOME)/.cache/claude\"..."; \
if command -v sudo >/dev/null 2>&1; then \
sudo chown -R "$(UID):$(GID)" "$(HOME)/.cache/claude"; \
else \
chown -R "$(UID):$(GID)" "$(HOME)/.cache/claude"; \
fi; \
fi
install -d -m 700 -- "$(HOME)/.cache/claude"
curl -fsSL https://claude.ai/install.sh | bash
dep-go:
go build -v ./...
dep-upgrade:
@ -420,7 +371,6 @@ zip-nsfw:
(cd assets && zip -r nsfw.zip nsfw -x "*/.*" -x "*/version.txt")
build-js:
(cd frontend && env BUILD_ENV=production NODE_ENV=production npm run build)
(cd frontend && node scripts/precompress.js)
build-go: build-develop
build-develop:
rm -f $(BINARY_NAME)
@ -439,43 +389,32 @@ build-static:
scripts/build.sh static $(BINARY_NAME)
build-libheif: build-libheif-amd64 build-libheif-arm64 build-libheif-armv7
build-libheif-amd64:
docker run --rm -u $(UID) --platform=amd64 --pull=always -v ".:/go/src/github.com/photoprism/photoprism" -e BUILD_ARCH=amd64 -e SYSTEM_ARCH=amd64 photoprism/develop:resolute ./scripts/dist/build-libheif.sh v1.23.1
docker run --rm -u $(UID) --platform=amd64 --pull=always -v ".:/go/src/github.com/photoprism/photoprism" -e BUILD_ARCH=amd64 -e SYSTEM_ARCH=amd64 photoprism/develop:questing ./scripts/dist/build-libheif.sh v1.23.1
docker run --rm -u $(UID) --platform=amd64 --pull=always -v ".:/go/src/github.com/photoprism/photoprism" -e BUILD_ARCH=amd64 -e SYSTEM_ARCH=amd64 photoprism/develop:plucky ./scripts/dist/build-libheif.sh v1.23.1
docker run --rm -u $(UID) --platform=amd64 --pull=always -v ".:/go/src/github.com/photoprism/photoprism" -e BUILD_ARCH=amd64 -e SYSTEM_ARCH=amd64 photoprism/develop:noble ./scripts/dist/build-libheif.sh v1.23.1
docker run --rm -u $(UID) --platform=amd64 --pull=always -v ".:/go/src/github.com/photoprism/photoprism" -e BUILD_ARCH=amd64 -e SYSTEM_ARCH=amd64 photoprism/develop:jammy ./scripts/dist/build-libheif.sh v1.23.1
docker run --rm -u $(UID) --platform=amd64 --pull=always -v ".:/go/src/github.com/photoprism/photoprism" -e BUILD_ARCH=amd64 -e SYSTEM_ARCH=amd64 photoprism/develop:bookworm ./scripts/dist/build-libheif.sh v1.23.1
docker run --rm -u $(UID) --platform=amd64 --pull=always -v ".:/go/src/github.com/photoprism/photoprism" -e BUILD_ARCH=amd64 -e SYSTEM_ARCH=amd64 photoprism/develop:trixie ./scripts/dist/build-libheif.sh v1.23.1
docker run --rm -u $(UID) --platform=amd64 --pull=always -v ".:/go/src/github.com/photoprism/photoprism" -e BUILD_ARCH=amd64 -e SYSTEM_ARCH=amd64 photoprism/develop:questing ./scripts/dist/build-libheif.sh v1.20.2
docker run --rm -u $(UID) --platform=amd64 --pull=always -v ".:/go/src/github.com/photoprism/photoprism" -e BUILD_ARCH=amd64 -e SYSTEM_ARCH=amd64 photoprism/develop:plucky ./scripts/dist/build-libheif.sh v1.20.2
docker run --rm -u $(UID) --platform=amd64 --pull=always -v ".:/go/src/github.com/photoprism/photoprism" -e BUILD_ARCH=amd64 -e SYSTEM_ARCH=amd64 photoprism/develop:noble ./scripts/dist/build-libheif.sh v1.20.2
docker run --rm -u $(UID) --platform=amd64 --pull=always -v ".:/go/src/github.com/photoprism/photoprism" -e BUILD_ARCH=amd64 -e SYSTEM_ARCH=amd64 photoprism/develop:jammy ./scripts/dist/build-libheif.sh v1.20.2
docker run --rm -u $(UID) --platform=amd64 --pull=always -v ".:/go/src/github.com/photoprism/photoprism" -e BUILD_ARCH=amd64 -e SYSTEM_ARCH=amd64 photoprism/develop:bookworm ./scripts/dist/build-libheif.sh v1.20.2
build-libheif-arm64:
docker run --rm -u $(UID) --platform=arm64 --pull=always -v ".:/go/src/github.com/photoprism/photoprism" -e BUILD_ARCH=arm64 -e SYSTEM_ARCH=arm64 photoprism/develop:resolute ./scripts/dist/build-libheif.sh v1.23.1
docker run --rm -u $(UID) --platform=arm64 --pull=always -v ".:/go/src/github.com/photoprism/photoprism" -e BUILD_ARCH=arm64 -e SYSTEM_ARCH=arm64 photoprism/develop:questing ./scripts/dist/build-libheif.sh v1.23.1
docker run --rm -u $(UID) --platform=arm64 --pull=always -v ".:/go/src/github.com/photoprism/photoprism" -e BUILD_ARCH=arm64 -e SYSTEM_ARCH=arm64 photoprism/develop:plucky ./scripts/dist/build-libheif.sh v1.23.1
docker run --rm -u $(UID) --platform=arm64 --pull=always -v ".:/go/src/github.com/photoprism/photoprism" -e BUILD_ARCH=arm64 -e SYSTEM_ARCH=arm64 photoprism/develop:noble ./scripts/dist/build-libheif.sh v1.23.1
docker run --rm -u $(UID) --platform=arm64 --pull=always -v ".:/go/src/github.com/photoprism/photoprism" -e BUILD_ARCH=arm64 -e SYSTEM_ARCH=arm64 photoprism/develop:jammy ./scripts/dist/build-libheif.sh v1.23.1
docker run --rm -u $(UID) --platform=arm64 --pull=always -v ".:/go/src/github.com/photoprism/photoprism" -e BUILD_ARCH=arm64 -e SYSTEM_ARCH=arm64 photoprism/develop:bookworm ./scripts/dist/build-libheif.sh v1.23.1
docker run --rm -u $(UID) --platform=arm64 --pull=always -v ".:/go/src/github.com/photoprism/photoprism" -e BUILD_ARCH=arm64 -e SYSTEM_ARCH=arm64 photoprism/develop:trixie ./scripts/dist/build-libheif.sh v1.23.1
docker run --rm -u $(UID) --platform=arm64 --pull=always -v ".:/go/src/github.com/photoprism/photoprism" -e BUILD_ARCH=arm64 -e SYSTEM_ARCH=arm64 photoprism/develop:questing ./scripts/dist/build-libheif.sh v1.20.2
docker run --rm -u $(UID) --platform=arm64 --pull=always -v ".:/go/src/github.com/photoprism/photoprism" -e BUILD_ARCH=arm64 -e SYSTEM_ARCH=arm64 photoprism/develop:plucky ./scripts/dist/build-libheif.sh v1.20.2
docker run --rm -u $(UID) --platform=arm64 --pull=always -v ".:/go/src/github.com/photoprism/photoprism" -e BUILD_ARCH=arm64 -e SYSTEM_ARCH=arm64 photoprism/develop:noble ./scripts/dist/build-libheif.sh v1.20.2
docker run --rm -u $(UID) --platform=arm64 --pull=always -v ".:/go/src/github.com/photoprism/photoprism" -e BUILD_ARCH=arm64 -e SYSTEM_ARCH=arm64 photoprism/develop:jammy ./scripts/dist/build-libheif.sh v1.20.2
docker run --rm -u $(UID) --platform=arm64 --pull=always -v ".:/go/src/github.com/photoprism/photoprism" -e BUILD_ARCH=arm64 -e SYSTEM_ARCH=arm64 photoprism/develop:bookworm ./scripts/dist/build-libheif.sh v1.20.2
build-libheif-armv7:
docker run --rm -u $(UID) --platform=arm --pull=always -v ".:/go/src/github.com/photoprism/photoprism" -e BUILD_ARCH=arm -e SYSTEM_ARCH=arm photoprism/develop:armv7 ./scripts/dist/build-libheif.sh v1.23.1
docker run --rm -u $(UID) --platform=arm --pull=always -v ".:/go/src/github.com/photoprism/photoprism" -e BUILD_ARCH=arm -e SYSTEM_ARCH=arm photoprism/develop:armv7 ./scripts/dist/build-libheif.sh v1.20.2
build-libheif-latest: build-libheif-amd64-latest build-libheif-arm64-latest build-libheif-armv7-latest
build-libheif-amd64-latest:
docker run --rm -u $(UID) --platform=amd64 --pull=always -v ".:/go/src/github.com/photoprism/photoprism" -e BUILD_ARCH=amd64 -e SYSTEM_ARCH=amd64 photoprism/develop:resolute ./scripts/dist/build-libheif.sh
docker run --rm -u $(UID) --platform=amd64 --pull=always -v ".:/go/src/github.com/photoprism/photoprism" -e BUILD_ARCH=amd64 -e SYSTEM_ARCH=amd64 photoprism/develop:questing ./scripts/dist/build-libheif.sh
docker run --rm -u $(UID) --platform=amd64 --pull=always -v ".:/go/src/github.com/photoprism/photoprism" -e BUILD_ARCH=amd64 -e SYSTEM_ARCH=amd64 photoprism/develop:plucky ./scripts/dist/build-libheif.sh
docker run --rm -u $(UID) --platform=amd64 --pull=always -v ".:/go/src/github.com/photoprism/photoprism" -e BUILD_ARCH=amd64 -e SYSTEM_ARCH=amd64 photoprism/develop:noble ./scripts/dist/build-libheif.sh
docker run --rm -u $(UID) --platform=amd64 --pull=always -v ".:/go/src/github.com/photoprism/photoprism" -e BUILD_ARCH=amd64 -e SYSTEM_ARCH=amd64 photoprism/develop:jammy ./scripts/dist/build-libheif.sh
build-libheif-arm64-latest:
docker run --rm -u $(UID) --platform=arm64 --pull=always -v ".:/go/src/github.com/photoprism/photoprism" -e BUILD_ARCH=arm64 -e SYSTEM_ARCH=arm64 photoprism/develop:resolute ./scripts/dist/build-libheif.sh
docker run --rm -u $(UID) --platform=arm64 --pull=always -v ".:/go/src/github.com/photoprism/photoprism" -e BUILD_ARCH=arm64 -e SYSTEM_ARCH=arm64 photoprism/develop:questing ./scripts/dist/build-libheif.sh
docker run --rm -u $(UID) --platform=arm64 --pull=always -v ".:/go/src/github.com/photoprism/photoprism" -e BUILD_ARCH=arm64 -e SYSTEM_ARCH=arm64 photoprism/develop:plucky ./scripts/dist/build-libheif.sh
docker run --rm -u $(UID) --platform=arm64 --pull=always -v ".:/go/src/github.com/photoprism/photoprism" -e BUILD_ARCH=arm64 -e SYSTEM_ARCH=arm64 photoprism/develop:noble ./scripts/dist/build-libheif.sh
docker run --rm -u $(UID) --platform=arm64 --pull=always -v ".:/go/src/github.com/photoprism/photoprism" -e BUILD_ARCH=arm64 -e SYSTEM_ARCH=arm64 photoprism/develop:jammy ./scripts/dist/build-libheif.sh
build-libheif-armv7-latest:
docker run --rm -u $(UID) --platform=arm --pull=always -v ".:/go/src/github.com/photoprism/photoprism" -e BUILD_ARCH=arm -e SYSTEM_ARCH=arm photoprism/develop:armv7 ./scripts/dist/build-libheif.sh
build-libheif-deb: build-libheif-deb-amd64 build-libheif-deb-arm64
build-libheif-deb-amd64:
docker run --rm -u $(UID) --platform=amd64 --pull=always -v ".:/go/src/github.com/photoprism/photoprism" -e BUILD_ARCH=amd64 -e SYSTEM_ARCH=amd64 photoprism/develop:resolute ./scripts/dist/build-libheif-deb.sh v1.23.1
build-libheif-deb-arm64:
docker run --rm -u $(UID) --platform=arm64 --pull=always -v ".:/go/src/github.com/photoprism/photoprism" -e BUILD_ARCH=arm64 -e SYSTEM_ARCH=arm64 photoprism/develop:resolute ./scripts/dist/build-libheif-deb.sh v1.23.1
build-tensorflow: docker-tensorflow-amd64
docker-tensorflow: docker-tensorflow-amd64
docker-tensorflow-amd64:
@ -494,30 +433,26 @@ build-setup: build-setup-nas-raspberry-pi
build-setup-nas-raspberry-pi:
./scripts/setup/nas/raspberry-pi/build.sh
watch-js:
(cd frontend && node scripts/precompress.js --clean)
(cd frontend && env BUILD_ENV=development NODE_ENV=production npm run watch)
test-js:
$(info Running JS unit tests...)
(cd frontend && npm run test)
acceptance:
$(info Running public-mode tests in Chrome...)
(cd frontend && find ./tests/acceptance -type f -name "*.js" | xargs -i perl -0777 -ne 'while(/(?:mode: \"auth[^,]*\,)|(Multi-Window\:[A-Za-z 0-9\-_]*)/g){print "$$1\n" if ($$1);}' {} | xargs -I testname bash -c 'npm run testcafe -- "chrome --headless=new --use-gl=angle --use-angle=swiftshader --disable-features=LocalNetworkAccessChecks" --experimental-multiple-windows --test-meta mode=public --config-file ./.testcaferc.cjs --test "testname" "tests/acceptance"')
(cd frontend && npm run testcafe -- "chrome --headless=new --use-gl=angle --use-angle=swiftshader --disable-features=LocalNetworkAccessChecks" --test-grep "^(Common|Core)\:*" --test-meta mode=public --config-file ./.testcaferc.cjs "tests/acceptance")
(cd frontend && find ./tests/acceptance -type f -name "*.js" | xargs -i perl -0777 -ne 'while(/(?:mode: \"auth[^,]*\,)|(Multi-Window\:[A-Za-z 0-9\-_]*)/g){print "$$1\n" if ($$1);}' {} | xargs -I testname bash -c 'npm run testcafe -- "chrome --headless=new --use-gl=angle --use-angle=swiftshader --disable-features=LocalNetworkAccessChecks" --experimental-multiple-windows --test-meta mode=public --config-file ./testcaferc.json --test "testname" "tests/acceptance"')
(cd frontend && npm run testcafe -- "chrome --headless=new --use-gl=angle --use-angle=swiftshader --disable-features=LocalNetworkAccessChecks" --test-grep "^(Common|Core)\:*" --test-meta mode=public --config-file ./testcaferc.json "tests/acceptance")
acceptance-short:
$(info Running JS acceptance tests in Chrome...)
(cd frontend && find ./tests/acceptance -type f -name "*.js" | xargs -i perl -0777 -ne 'while(/(?:mode: \"auth[^,]*\,)|(Multi-Window\:[A-Za-z 0-9\-_]*)/g){print "$$1\n" if ($$1);}' {} | xargs -I testname bash -c 'npm run testcafe -- "chrome --headless=new --use-gl=angle --use-angle=swiftshader --disable-features=LocalNetworkAccessChecks" --experimental-multiple-windows --test-meta mode=public,type=short --config-file ./.testcaferc.cjs --test "testname" "tests/acceptance"')
(cd frontend && npm run testcafe -- "chrome --headless=new --use-gl=angle --use-angle=swiftshader --disable-features=LocalNetworkAccessChecks" --test-grep "^(Common|Core)\:*" --test-meta mode=public,type=short --config-file ./.testcaferc.cjs "tests/acceptance")
(cd frontend && find ./tests/acceptance -type f -name "*.js" | xargs -i perl -0777 -ne 'while(/(?:mode: \"auth[^,]*\,)|(Multi-Window\:[A-Za-z 0-9\-_]*)/g){print "$$1\n" if ($$1);}' {} | xargs -I testname bash -c 'npm run testcafe -- "chrome --headless=new --use-gl=angle --use-angle=swiftshader --disable-features=LocalNetworkAccessChecks" --experimental-multiple-windows --test-meta mode=public,type=short --config-file ./testcaferc.json --test "testname" "tests/acceptance"')
(cd frontend && npm run testcafe -- "chrome --headless=new --use-gl=angle --use-angle=swiftshader --disable-features=LocalNetworkAccessChecks" --test-grep "^(Common|Core)\:*" --test-meta mode=public,type=short --config-file ./testcaferc.json "tests/acceptance")
acceptance-auth:
$(info Running JS acceptance-auth tests in Chrome...)
(cd frontend && find ./tests/acceptance -type f -name "*.js" | xargs -i perl -0777 -ne 'while(/(?:mode: \"public[^,]*\,)|(Multi-Window\:[A-Za-z 0-9\-_]*)/g){print "$$1\n" if ($$1);}' {} | xargs -I testname bash -c 'npm run testcafe -- "chrome --headless=new --use-gl=angle --use-angle=swiftshader --disable-features=LocalNetworkAccessChecks" --experimental-multiple-windows --test-meta mode=auth --config-file ./.testcaferc.cjs --test "testname" "tests/acceptance"')
(cd frontend && npm run testcafe -- "chrome --headless=new --use-gl=angle --use-angle=swiftshader --disable-features=LocalNetworkAccessChecks" --test-grep "^(Common|Core)\:*" --test-meta mode=auth --config-file ./.testcaferc.cjs "tests/acceptance")
(cd frontend && find ./tests/acceptance -type f -name "*.js" | xargs -i perl -0777 -ne 'while(/(?:mode: \"public[^,]*\,)|(Multi-Window\:[A-Za-z 0-9\-_]*)/g){print "$$1\n" if ($$1);}' {} | xargs -I testname bash -c 'npm run testcafe -- "chrome --headless=new --use-gl=angle --use-angle=swiftshader --disable-features=LocalNetworkAccessChecks" --experimental-multiple-windows --test-meta mode=auth --config-file ./testcaferc.json --test "testname" "tests/acceptance"')
(cd frontend && npm run testcafe -- "chrome --headless=new --use-gl=angle --use-angle=swiftshader --disable-features=LocalNetworkAccessChecks" --test-grep "^(Common|Core)\:*" --test-meta mode=auth --config-file ./testcaferc.json "tests/acceptance")
acceptance-auth-short:
$(info Running JS acceptance-auth tests in Chrome...)
(cd frontend && find ./tests/acceptance -type f -name "*.js" | xargs -i perl -0777 -ne 'while(/(?:mode: \"public[^,]*\,)|(Multi-Window\:[A-Za-z 0-9\-_]*)/g){print "$$1\n" if ($$1);}' {} | xargs -I testname bash -c 'npm run testcafe -- "chrome --headless=new --use-gl=angle --use-angle=swiftshader --disable-features=LocalNetworkAccessChecks" --experimental-multiple-windows --test-meta mode=auth,type=short --config-file ./.testcaferc.cjs --test "testname" "tests/acceptance"')
(cd frontend && npm run testcafe -- "chrome --headless=new --use-gl=angle --use-angle=swiftshader --disable-features=LocalNetworkAccessChecks" --test-grep "^(Common|Core)\:*" --test-meta mode=auth,type=short --config-file ./.testcaferc.cjs "tests/acceptance")
acceptance-api:
$(info Running JS acceptance-api tests in Chrome...)
(cd frontend && npm run testcafe -- "chrome --headless=new --use-gl=angle --use-angle=swiftshader --disable-features=LocalNetworkAccessChecks" --test-meta mode=api --config-file ./.testcaferc.cjs "tests/acceptance")
(cd frontend && find ./tests/acceptance -type f -name "*.js" | xargs -i perl -0777 -ne 'while(/(?:mode: \"public[^,]*\,)|(Multi-Window\:[A-Za-z 0-9\-_]*)/g){print "$$1\n" if ($$1);}' {} | xargs -I testname bash -c 'npm run testcafe -- "chrome --headless=new --use-gl=angle --use-angle=swiftshader --disable-features=LocalNetworkAccessChecks" --experimental-multiple-windows --test-meta mode=auth,type=short --config-file ./testcaferc.json --test "testname" "tests/acceptance"')
(cd frontend && npm run testcafe -- "chrome --headless=new --use-gl=angle --use-angle=swiftshader --disable-features=LocalNetworkAccessChecks" --test-grep "^(Common|Core)\:*" --test-meta mode=auth,type=short --config-file ./testcaferc.json "tests/acceptance")
vitest-watch:
$(info Running Vitest unit tests in watch mode...)
(cd frontend && npm run test-watch)
@ -556,7 +491,7 @@ run-test-hub:
env PHOTOPRISM_TEST_HUB="true" $(GOTEST) -parallel 1 -count 1 -cpu 1 -tags="slow,develop,debug" -timeout 20m ./pkg/... ./internal/...
run-test-mariadb:
$(info Running all Go tests on MariaDB...)
PHOTOPRISM_TEST_DRIVER="mysql" PHOTOPRISM_TEST_DSN="root:photoprism@tcp(mariadb:$${MARIADB_PORT:-4001})/acceptance?charset=utf8mb4,utf8&collation=utf8mb4_unicode_ci&parseTime=true" $(GOTEST) -parallel 1 -count 1 -cpu 1 -tags="slow,develop" -timeout 20m ./pkg/... ./internal/...
PHOTOPRISM_TEST_DRIVER="mysql" PHOTOPRISM_TEST_DSN="root:photoprism@tcp(mariadb:4001)/acceptance?charset=utf8mb4,utf8&collation=utf8mb4_unicode_ci&parseTime=true" $(GOTEST) -parallel 1 -count 1 -cpu 1 -tags="slow,develop" -timeout 20m ./pkg/... ./internal/...
run-test-pkg:
$(info Running all Go tests in "/pkg"...)
$(GOTEST) -parallel 2 -count 1 -cpu 2 -tags="slow,develop" -timeout 20m ./pkg/...
@ -611,7 +546,7 @@ docker-pull:
$(DOCKER_COMPOSE) -f compose.latest.yaml pull --ignore-pull-failures
build-docker: docker-build
docker-build:
$(DOCKER_COMPOSE) --profile=postgres --profile=keycloak pull --ignore-pull-failures
$(DOCKER_COMPOSE) --profile=all pull --ignore-pull-failures
$(DOCKER_COMPOSE) down --remove-orphans
$(DOCKER_COMPOSE) build --pull
nvidia: nvidia-up
@ -637,8 +572,7 @@ docker-develop: docker-develop-latest
docker-develop-all: docker-develop-latest docker-develop-other
docker-develop-latest: docker-develop-ubuntu
docker-develop-debian: docker-develop-bookworm docker-develop-bookworm-slim
docker-develop-ubuntu: docker-develop-resolute docker-develop-resolute-slim
docker-develop-legacy: docker-develop-jammy docker-develop-jammy-slim
docker-develop-ubuntu: docker-develop-questing docker-develop-questing-slim
docker-develop-other: docker-develop-debian docker-develop-bullseye docker-develop-bullseye-slim docker-develop-buster
docker-develop-bookworm:
docker pull --platform=amd64 debian:bookworm-slim
@ -649,14 +583,6 @@ docker-develop-bookworm-slim:
docker pull --platform=amd64 debian:bookworm-slim
docker pull --platform=arm64 debian:bookworm-slim
scripts/docker/buildx-multi.sh develop linux/amd64,linux/arm64 bookworm-slim /bookworm-slim
docker-develop-trixie:
docker pull --platform=amd64 debian:trixie-slim
docker pull --platform=arm64 debian:trixie-slim
scripts/docker/buildx-multi.sh develop linux/amd64,linux/arm64 trixie /trixie
docker-develop-trixie-slim:
docker pull --platform=amd64 debian:trixie-slim
docker pull --platform=arm64 debian:trixie-slim
scripts/docker/buildx-multi.sh develop linux/amd64,linux/arm64 trixie-slim /trixie-slim
docker-develop-bullseye:
docker pull --platform=amd64 golang:1-bullseye
docker pull --platform=arm64 golang:1-bullseye
@ -729,19 +655,11 @@ docker-develop-plucky-slim:
docker-develop-questing:
docker pull --platform=amd64 ubuntu:questing
docker pull --platform=arm64 ubuntu:questing
scripts/docker/buildx-multi.sh develop linux/amd64,linux/arm64 questing /questing
scripts/docker/buildx-multi.sh develop linux/amd64,linux/arm64 questing /questing "-t photoprism/develop:latest -t photoprism/develop:ubuntu"
docker-develop-questing-slim:
docker pull --platform=amd64 ubuntu:questing
docker pull --platform=arm64 ubuntu:questing
scripts/docker/buildx-multi.sh develop linux/amd64,linux/arm64 questing-slim /questing-slim
docker-develop-resolute:
docker pull --platform=amd64 ubuntu:resolute
docker pull --platform=arm64 ubuntu:resolute
scripts/docker/buildx-multi.sh develop linux/amd64,linux/arm64 resolute /resolute "-t photoprism/develop:latest -t photoprism/develop:ubuntu"
docker-develop-resolute-slim:
docker pull --platform=amd64 ubuntu:resolute
docker pull --platform=arm64 ubuntu:resolute
scripts/docker/buildx-multi.sh develop linux/amd64,linux/arm64 resolute-slim /resolute-slim
unstable: docker-unstable
docker-unstable: docker-unstable-mantic
docker-unstable-jammy:
@ -759,10 +677,10 @@ docker-unstable-mantic:
preview: docker-preview-ce
docker-preview: docker-preview-ce
docker-preview-all: docker-preview-latest docker-preview-other
docker-preview-ce: docker-preview-resolute
docker-preview-ce: docker-preview-questing
docker-preview-latest: docker-preview-ubuntu
docker-preview-debian: docker-preview-bookworm
docker-preview-ubuntu: docker-preview-resolute
docker-preview-ubuntu: docker-preview-questing
docker-preview-other: docker-preview-debian docker-preview-bullseye
docker-preview-arm: docker-preview-arm64 docker-preview-armv7
docker-preview-bookworm:
@ -838,19 +756,13 @@ docker-preview-questing:
docker pull --platform=amd64 photoprism/develop:questing-slim
docker pull --platform=arm64 photoprism/develop:questing
docker pull --platform=arm64 photoprism/develop:questing-slim
scripts/docker/buildx-multi.sh photoprism linux/amd64,linux/arm64 preview-questing /questing
docker-preview-resolute:
docker pull --platform=amd64 photoprism/develop:resolute
docker pull --platform=amd64 photoprism/develop:resolute-slim
docker pull --platform=arm64 photoprism/develop:resolute
docker pull --platform=arm64 photoprism/develop:resolute-slim
scripts/docker/buildx-multi.sh photoprism linux/amd64,linux/arm64 preview-ce /resolute
scripts/docker/buildx-multi.sh photoprism linux/amd64,linux/arm64 preview-ce /questing
release: docker-release
docker-release: docker-release-latest
docker-release-all: docker-release-latest docker-release-other
docker-release-latest: docker-release-ubuntu
docker-release-debian: docker-release-bookworm
docker-release-ubuntu: docker-release-resolute
docker-release-ubuntu: docker-release-questing
docker-release-other: docker-release-debian docker-release-bullseye
docker-release-arm: docker-release-arm64 docker-release-armv7
docker-release-bookworm:
@ -926,13 +838,7 @@ docker-release-questing:
docker pull --platform=amd64 photoprism/develop:questing-slim
docker pull --platform=arm64 photoprism/develop:questing
docker pull --platform=arm64 photoprism/develop:questing-slim
scripts/docker/buildx-multi.sh photoprism linux/amd64,linux/arm64 ce-questing /questing
docker-release-resolute:
docker pull --platform=amd64 photoprism/develop:resolute
docker pull --platform=amd64 photoprism/develop:resolute-slim
docker pull --platform=arm64 photoprism/develop:resolute
docker pull --platform=arm64 photoprism/develop:resolute-slim
scripts/docker/buildx-multi.sh photoprism linux/amd64,linux/arm64 ce /resolute
scripts/docker/buildx-multi.sh photoprism linux/amd64,linux/arm64 ce /questing
start-traefik:
$(DOCKER_COMPOSE) up -d --wait traefik
stop-traefik:
@ -979,20 +885,16 @@ terminal-preview:
$(DOCKER_COMPOSE) -f compose.preview.yaml exec photoprism-preview bash
logs-preview:
$(DOCKER_COMPOSE) -f compose.preview.yaml logs -f photoprism-preview
docker-local: docker-local-resolute
docker-local: docker-local-questing
docker-local-up:
$(DOCKER_COMPOSE) -f compose.local.yaml up --force-recreate
docker-local-down:
$(DOCKER_COMPOSE) -f compose.local.yaml down --remove-orphans
docker-local-all: docker-local-resolute docker-local-questing docker-local-plucky docker-local-oracular docker-local-noble docker-local-mantic docker-local-lunar docker-local-jammy docker-local-bookworm docker-local-bullseye docker-local-buster
docker-local-all: docker-local-questing docker-local-plucky docker-local-oracular docker-local-noble docker-local-mantic docker-local-lunar docker-local-jammy docker-local-bookworm docker-local-bullseye docker-local-buster
docker-local-bookworm:
docker pull photoprism/develop:bookworm
docker pull photoprism/develop:bookworm-slim
scripts/docker/build.sh photoprism ce-bookworm /bookworm "-t photoprism/photoprism:local"
docker-local-trixie:
docker pull photoprism/develop:trixie
docker pull debian:trixie-slim
scripts/docker/build.sh photoprism ce-trixie /trixie "-t photoprism/photoprism:local"
docker-local-bullseye:
docker pull photoprism/develop:bullseye
docker pull photoprism/develop:bullseye-slim
@ -1033,19 +935,12 @@ docker-local-plucky:
docker pull photoprism/develop:plucky
docker pull ubuntu:plucky
scripts/docker/build.sh photoprism ce-plucky /plucky "-t photoprism/photoprism:local"
docker-local-resolute:
docker pull photoprism/develop:resolute
docker pull ubuntu:resolute
scripts/docker/build.sh photoprism ce-resolute /resolute "-t photoprism/photoprism:local"
local-develop: docker-local-develop
docker-local-develop: docker-local-develop-resolute
docker-local-develop-all: docker-local-develop-resolute docker-local-develop-questing docker-local-develop-oracular docker-local-develop-noble docker-local-develop-mantic docker-local-develop-lunar docker-local-develop-jammy docker-local-develop-bookworm docker-local-develop-bullseye docker-local-develop-buster docker-local-develop-impish
docker-local-develop: docker-local-develop-questing
docker-local-develop-all: docker-local-develop-questing docker-local-develop-oracular docker-local-develop-noble docker-local-develop-mantic docker-local-develop-lunar docker-local-develop-jammy docker-local-develop-bookworm docker-local-develop-bullseye docker-local-develop-buster docker-local-develop-impish
docker-local-develop-bookworm:
docker pull debian:bookworm-slim
scripts/docker/build.sh develop bookworm /bookworm
docker-local-develop-trixie:
docker pull debian:trixie-slim
scripts/docker/build.sh develop trixie /trixie
docker-local-develop-bullseye:
docker pull golang:1-bullseye
scripts/docker/build.sh develop bullseye /bullseye
@ -1076,12 +971,12 @@ docker-local-develop-questing:
docker-local-develop-plucky:
docker pull ubuntu:plucky
scripts/docker/build.sh develop plucky /plucky
docker-local-develop-resolute:
docker pull ubuntu:resolute
scripts/docker/build.sh develop resolute /resolute
docker-ddns:
docker pull golang:alpine
scripts/docker/buildx-multi.sh ddns linux/amd64,linux/arm64 $(BUILD_DATE)
docker-goproxy:
docker pull golang:alpine
scripts/docker/buildx-multi.sh goproxy linux/amd64,linux/arm64 $(BUILD_DATE)
demo: docker-demo
docker-demo: docker-demo-latest
docker-demo-all: docker-demo-latest docker-demo-debian
@ -1116,16 +1011,13 @@ docker-dummy-oidc:
packer-digitalocean:
$(info Buildinng DigitalOcean marketplace image...)
(cd ./setup/docker/cloud && packer build digitalocean.json)
lint: lint-js lint-go check-api-request-limits
lint: lint-js lint-go
lint-js:
$(info Linting JS code...)
$(MAKE) -C frontend lint
lint-go:
$(info Linting Go code...)
golangci-lint run --issues-exit-code 0 ./pkg/... ./internal/... ./.../internal/...
check-api-request-limits:
$(info Checking API request-body limits...)
bash ./scripts/check-api-request-limits.sh
fmt-js:
(cd frontend && npm run fmt)
fmt-go:

1670
NOTICE

File diff suppressed because it is too large Load diff

View file

@ -8,7 +8,9 @@ PhotoPrism: Browse Your Life in Pictures
[![Bluesky Social](https://dl.photoprism.app/img/badges/badge-bluesky.svg)](https://bsky.app/profile/photoprism.app)
[![Mastodon](https://dl.photoprism.app/img/badges/badge-floss-social.svg)](https://floss.social/@photoprism)
PhotoPrism® is an AI-powered, privacy-first app for browsing, organizing, and sharing photos and videos. It helps tag, search, and rediscover media without getting in your way, whether self-hosted or in the cloud.
PhotoPrism® is an AI-Powered Photos App for the [Decentralized Web](https://en.wikipedia.org/wiki/Decentralized_web).
It makes use of the latest technologies to tag and find pictures automatically without getting in your way.
You can run it at home, on a private server, or in the cloud.
![](https://dl.photoprism.app/img/ui/2025/desktop-search.jpg)
@ -20,7 +22,7 @@ To get a first impression, you are welcome to play with our [public demo](https:
<img align="right" height="270" src="https://dl.photoprism.app/img/ui/2025/iphone-crocus-540px.png">
* Browse [all your pictures](https://docs.photoprism.app/user-guide/organize/browse/) without worrying about [RAW images](https://www.photoprism.app/kb/file-formats/) or [video formats](https://docs.photoprism.app/user-guide/organize/video/)
* Browse [all your pictures](https://docs.photoprism.app/user-guide/organize/browse/) without worrying about [RAW images](https://www.photoprism.app/kb/file-formats) or [video formats](https://docs.photoprism.app/user-guide/organize/video/)
* Whether you're using a phone, tablet, or desktop computer, our [intuitive PWA](https://try.photoprism.app/) provides a native app-like experience and can be [easily installed](https://docs.photoprism.app/user-guide/pwa/) on your home screen
* Quickly find specific photos and videos with [powerful search filters](https://docs.photoprism.app/user-guide/search/filters/) that can be combined and are available for [many different properties](https://docs.photoprism.app/user-guide/search/filters/#filter-reference), including [labels](https://try.photoprism.app/library/labels), [location](https://try.photoprism.app/library/places?q=s2:47a85a63f764), [resolution](https://try.photoprism.app/library/browse?view=cards&q=mp:4), [color](https://try.photoprism.app/library/browse?view=cards&q=color:red), [chroma](https://try.photoprism.app/library/browse?view=cards&q=mono%3Atrue), and [quality](https://try.photoprism.app/library/review)
* [Automatically labels your pictures](https://try.photoprism.app/library/labels) based on content and location, and recognizes the faces of [your family and friends](https://try.photoprism.app/library/people/new)
@ -30,7 +32,7 @@ To get a first impression, you are welcome to play with our [public demo](https:
* [Use compatible apps](https://docs.photoprism.app/user-guide/native-apps/) like [PhotoSync](https://link.photoprism.app/photosync) to back up iOS and Android phones in the background
* WebDAV clients such as [Microsoft's Windows Explorer](https://docs.photoprism.app/user-guide/sync/webdav/#__tabbed_1_2) and [Apple's Finder](https://docs.photoprism.app/user-guide/sync/webdav/#connect-to-a-webdav-server) can [connect directly to PhotoPrism](https://docs.photoprism.app/user-guide/sync/webdav/), allowing you to open, edit, and delete files from your computer as if they were local
Being completely [**self-funded and independent**](https://link.photoprism.app/membership), we can promise you that we will [never sell your data](https://www.photoprism.app/privacy/) and that we will [always be transparent](https://www.photoprism.app/terms/) about our software and services. Your data will never be shared with Google, Amazon, Microsoft or Apple unless you intentionally upload files to one of their services. 🔒
Being completely [**self-funded and independent**](https://link.photoprism.app/membership), we can promise you that we will [never sell your data](https://www.photoprism.app/privacy) and that we will [always be transparent](https://www.photoprism.app/terms) about our software and services. Your data will never be shared with Google, Amazon, Microsoft or Apple unless you intentionally upload files to one of their services. 🔒
## Getting Started ##
@ -43,18 +45,18 @@ See our [Getting Started FAQ](https://docs.photoprism.app/getting-started/faq/#h
## Support Our Mission 💎 ##
**PhotoPrism is 100% self-funded and independent.** Your [continued support](https://link.photoprism.app/membership) helps us [provide more features to the public](https://www.photoprism.app/oss/faq/#what-functionality-is-generally-available), release [regular updates](https://docs.photoprism.app/release-notes/), and remain independent!
**PhotoPrism is 100% self-funded and independent.** Your [continued support](https://link.photoprism.app/membership) helps us [provide more features to the public](https://www.photoprism.app/oss/faq#what-functionality-is-generally-available), release [regular updates](https://docs.photoprism.app/release-notes/), and remain independent!
Our members [enjoy additional features](https://www.photoprism.app/kb/personal/), including access to [interactive world maps](https://try.photoprism.app/library/places), and can join our private chat room to [connect with our team](https://www.photoprism.app/about/team/). We currently have the following membership options:
Our members [enjoy additional features](https://www.photoprism.app/kb/personal), including access to [interactive world maps](https://try.photoprism.app/library/places), and can join our private chat room to [connect with our team](https://www.photoprism.app/about/team). We currently have the following membership options:
- You can [sign up directly on our website](https://link.photoprism.app/membership) and pay with credit card or SEPA through Stripe, so you don't need to [link an external account](https://www.photoprism.app/kb/activation/) and can easily upgrade or downgrade at any time
- You can [sign up directly on our website](https://link.photoprism.app/membership) and pay with credit card or SEPA through Stripe, so you don't need to [link an external account](https://www.photoprism.app/kb/activation) and can easily upgrade or downgrade at any time
- Alternatively, [Patreon](https://link.photoprism.app/patreon) also supports PayPal, additional currencies, and lets you choose between monthly and annual billing for all tiers
If you currently support us through [GitHub Sponsors](https://link.photoprism.app/sponsor), you can also [register on our website](https://my.photoprism.app/register) and use the *Activate GitHub Sponsors Membership* button to link your account. For details on this and how to [link your Patreon account](https://www.patreon.com/pledges), see our [Activation Guide](https://www.photoprism.app/kb/activation/).
If you currently support us through [GitHub Sponsors](https://link.photoprism.app/sponsor), you can also [register on our website](https://my.photoprism.app/register) and use the *Activate GitHub Sponsors Membership* button to link your account. For details on this and how to [link your Patreon account](https://www.patreon.com/pledges), see our [Activation Guide](https://www.photoprism.app/kb/activation).
You are [welcome to contact us](https://www.photoprism.app/contact/) for change requests, membership questions, and business partnerships.
You are [welcome to contact us](https://www.photoprism.app/contact) for change requests, membership questions, and business partnerships.
[View Membership FAQ ](https://www.photoprism.app/membership/faq/)[Sign Up ](https://link.photoprism.app/membership)
[View Membership FAQ ](https://www.photoprism.app/kb/membership)[Sign Up ](https://link.photoprism.app/membership)
### Why Your Support Matters ###
@ -81,7 +83,7 @@ Our [Project Roadmap](https://link.photoprism.app/roadmap) shows what tasks are
Be aware that we have a zero-bug policy and do our best to help users when they need support or have other questions. This comes at a price though, as we can't give exact release dates for new features. Our team receives many more requests than can be implemented, so we want to emphasize that we are in no way obligated to implement the features, enhancements, or other changes you request. We do, however, appreciate your feedback and carefully consider all requests.
**Because sustained funding is key to quickly releasing new features, we encourage you to support our mission by [signing up for a personal membership](https://link.photoprism.app/membership) or [purchasing a commercial license](https://www.photoprism.app/teams/#compare).**
**Because sustained funding is key to quickly releasing new features, we encourage you to support our mission by [signing up for a personal membership](https://link.photoprism.app/membership) or [purchasing a commercial license](https://www.photoprism.app/teams#compare).**
[Become a Member ](https://link.photoprism.app/membership)
@ -89,18 +91,18 @@ Be aware that we have a zero-bug policy and do our best to help users when they
We kindly ask you not to report bugs via GitHub Issues **unless you are certain to have found a fully reproducible and previously unreported issue** that must be fixed directly in the app. Thank you for your careful consideration!
- When browsing issues, please note that **our team and all issue subscribers receive an email notification** from GitHub whenever a new comment is added, so these should only be used for sharing important information and not for [discussions, questions](https://github.com/photoprism/photoprism/discussions), or [expressing personal opinions](https://www.photoprism.app/code-of-conduct/)
- In order for us to investigate [new bug reports](https://www.photoprism.app/kb/reporting-bugs/), they must include **a complete list of steps to reproduce the problem**, the software versions used and information about the environment in which the problem occurred, such as [browser type, browser version, browser plug-ins](https://docs.photoprism.app/getting-started/troubleshooting/browsers/), operating system, [storage type](https://docs.photoprism.app/getting-started/troubleshooting/performance/#storage), [processor type](https://docs.photoprism.app/getting-started/troubleshooting/performance/#server-cpu), and [memory size](https://docs.photoprism.app/getting-started/troubleshooting/performance/#memory)
- [Contact us](https://www.photoprism.app/contact/) or [a community member](https://link.photoprism.app/discussions) if you need help, it could be a local configuration problem, or a misunderstanding in how the software works
- When browsing issues, please note that **our team and all issue subscribers receive an email notification** from GitHub whenever a new comment is added, so these should only be used for sharing important information and not for [discussions, questions](https://github.com/photoprism/photoprism/discussions), or [expressing personal opinions](https://www.photoprism.app/code-of-conduct)
- In order for us to investigate [new bug reports](https://www.photoprism.app/kb/reporting-bugs), they must include **a complete list of steps to reproduce the problem**, the software versions used and information about the environment in which the problem occurred, such as [browser type, browser version, browser plug-ins](https://docs.photoprism.app/getting-started/troubleshooting/browsers/), operating system, [storage type](https://docs.photoprism.app/getting-started/troubleshooting/performance/#storage), [processor type](https://docs.photoprism.app/getting-started/troubleshooting/performance/#server-cpu), and [memory size](https://docs.photoprism.app/getting-started/troubleshooting/performance/#memory)
- [Contact us](https://www.photoprism.app/contact) or [a community member](https://link.photoprism.app/discussions) if you need help, it could be a local configuration problem, or a misunderstanding in how the software works
- This gives us the opportunity to [improve our documentation](https://docs.photoprism.app/getting-started/troubleshooting/) and provide best-in-class support instead of dealing with unclear/duplicate bug reports or triggering a flood of notifications by replying to comments
## Connect with the Community ##
<a href="https://link.photoprism.app/chat"><img align="right" width="144" height="144" src="https://dl.photoprism.app/img/brands/element-logo.svg"></a>
Follow us on [Mastodon](https://floss.social/@photoprism), [Bluesky](https://bsky.app/profile/photoprism.app), or join the [Community Chat](https://link.photoprism.app/chat) to get regular updates, connect with other users, and discuss your ideas. Our [Code of Conduct](https://www.photoprism.app/code-of-conduct/) explains the "dos and donts" when interacting with other community members.
Follow us on [Mastodon](https://floss.social/@photoprism), [Bluesky](https://bsky.app/profile/photoprism.app), or join the [Community Chat](https://link.photoprism.app/chat) to get regular updates, connect with other users, and discuss your ideas. Our [Code of Conduct](https://www.photoprism.app/code-of-conduct) explains the "dos and donts" when interacting with other community members.
As a [contributor](CONTRIBUTING.md), you are also welcome to [contact us directly](https://www.photoprism.app/contact/) if you have something on your mind that you don't want to discuss publicly. Please note, however, that due to the high volume of emails we receive, our team may be unable to get back to you immediately. We do our best to respond within five business days or less.
As a [contributor](CONTRIBUTING.md), you are also welcome to [contact us directly](https://www.photoprism.app/contact) if you have something on your mind that you don't want to discuss publicly. Please note, however, that due to the high volume of emails we receive, our team may be unable to get back to you immediately. We do our best to respond within five business days or less.
## Every Contribution Makes a Difference ##
@ -108,4 +110,4 @@ We welcome [contributions](CONTRIBUTING.md) of any kind, including blog posts, t
----
*PhotoPrism® is a [registered trademark](https://www.photoprism.app/trademark/). By using the software and services we provide, you agree to our [Terms of Service](https://www.photoprism.app/terms/), [Privacy Policy](https://www.photoprism.app/privacy/), and [Code of Conduct](https://www.photoprism.app/code-of-conduct/). Docs are [available](https://link.photoprism.app/github-docs) under the [CC BY-NC-SA 4.0 License](https://creativecommons.org/licenses/by-nc-sa/4.0/); [additional terms](https://github.com/photoprism/photoprism/blob/develop/assets/README.md) may apply.*
*PhotoPrism® is a [registered trademark](https://www.photoprism.app/trademark). By using the software and services we provide, you agree to our [Terms of Service](https://www.photoprism.app/terms), [Privacy Policy](https://www.photoprism.app/privacy), and [Code of Conduct](https://www.photoprism.app/code-of-conduct). Docs are [available](https://link.photoprism.app/github-docs) under the [CC BY-NC-SA 4.0 License](https://creativecommons.org/licenses/by-nc-sa/4.0/); [additional terms](https://github.com/photoprism/photoprism/blob/develop/assets/README.md) may apply.*

View file

@ -1,10 +1,10 @@
# Thank You to All Current and Past Sponsors 💎 #
Your [continued support](https://link.photoprism.app/membership) helps us [provide more features to the public](https://www.photoprism.app/oss/faq/#what-functionality-is-generally-available), release [regular updates](https://docs.photoprism.app/release-notes/), and remain independent! 💜
Your [continued support](https://link.photoprism.app/membership) helps us [provide more features to the public](https://www.photoprism.app/oss/faq#what-functionality-is-generally-available), release [regular updates](https://docs.photoprism.app/release-notes/), and remain independent! 💜
You are [welcome to contact us](https://www.photoprism.app/contact/) for change requests, membership questions, and business partnerships.
You are [welcome to contact us](https://www.photoprism.app/contact) for change requests, membership questions, and business partnerships.
[View Membership FAQ ](https://www.photoprism.app/membership/faq/)[Contact Us ](https://www.photoprism.app/contact/)
[View Membership FAQ ](https://www.photoprism.app/kb/membership)[Contact Us ](https://www.photoprism.app/contact)
## Platinum Sponsors ##
@ -90,8 +90,6 @@ You are [welcome to contact us](https://www.photoprism.app/contact/) for change
[**Kevin "BentoFox" Gelking**](https://github.com/KevinGelking) (Patreon, December 2024)
[**Lukaz**](https://github.com/lukazvprecisn) (Patreon, May 2025)
[**Philipp Marmet**](https://github.com/fujexo) (October 2025)
## Infrastructure Sponsors ##

View file

@ -1,6 +1,5 @@
examples
README.md
Thumbs.db
docs
.*
_*
_*

View file

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 159 KiB

After

Width:  |  Height:  |  Size: 159 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 65 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 83 KiB

After

Width:  |  Height:  |  Size: 83 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 118 KiB

After

Width:  |  Height:  |  Size: 118 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 103 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 96 KiB

After

Width:  |  Height:  |  Size: 96 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 98 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 61 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 86 KiB

After

Width:  |  Height:  |  Size: 86 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 68 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 95 KiB

After

Width:  |  Height:  |  Size: 95 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 75 KiB

After

Width:  |  Height:  |  Size: 75 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 85 KiB

After

Width:  |  Height:  |  Size: 85 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 79 KiB

After

Width:  |  Height:  |  Size: 79 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 64 KiB

Before After
Before After

Some files were not shown because too many files have changed in this diff Show more