diff --git a/.agents/.gitignore b/.agents/.gitignore deleted file mode 100644 index c96a04f00..000000000 --- a/.agents/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -* -!.gitignore \ No newline at end of file diff --git a/.claude/.gitignore b/.claude/.gitignore deleted file mode 100644 index f09ddd2f8..000000000 --- a/.claude/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -* -!.gitignore -!CLAUDE.md -!rules -!rules/*.md -!agents -!agents/*.md -!settings.json \ No newline at end of file diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md deleted file mode 100644 index fe2232a13..000000000 --- a/.claude/CLAUDE.md +++ /dev/null @@ -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 ` — 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 ` and resolve warnings. When editing Markdown files that contain tables, format them with `npx --yes markdown-table-formatter `. - -## 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. diff --git a/.claude/agents/ui-tester.md b/.claude/agents/ui-tester.md deleted file mode 100644 index b523df4a6..000000000 --- a/.claude/agents/ui-tester.md +++ /dev/null @@ -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:` 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:** 3–6 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. \ No newline at end of file diff --git a/.claude/rules/api-and-config.md b/.claude/rules/api-and-config.md deleted file mode 100644 index bf64a8c9e..000000000 --- a/.claude/rules/api-and-config.md +++ /dev/null @@ -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("")` 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()` / `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`). diff --git a/.claude/rules/build-and-runtime.md b/.claude/rules/build-and-runtime.md deleted file mode 100644 index c9fd9d390..000000000 --- a/.claude/rules/build-and-runtime.md +++ /dev/null @@ -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 ` - - Run as non-root to avoid root-owned files: `docker compose exec -u "$(id -u):$(id -g)" photoprism ` -- 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:-` (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 `:` and `:-` 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. diff --git a/.claude/rules/cluster-operations.md b/.claude/rules/cluster-operations.md deleted file mode 100644 index a1641dc22..000000000 --- a/.claude/rules/cluster-operations.md +++ /dev/null @@ -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 (`d`, `u`; 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. diff --git a/.claude/rules/code-comments.md b/.claude/rules/code-comments.md deleted file mode 100644 index 97cde32ff..000000000 --- a/.claude/rules/code-comments.md +++ /dev/null @@ -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. diff --git a/.claude/rules/commit-and-docs-style.md b/.claude/rules/commit-and-docs-style.md deleted file mode 100644 index 284073b74..000000000 --- a/.claude/rules/commit-and-docs-style.md +++ /dev/null @@ -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 , I want , so that .**` -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/" ` 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. diff --git a/.claude/rules/frontend-rules.md b/.claude/rules/frontend-rules.md deleted file mode 100644 index 982f2e9f7..000000000 --- a/.claude/rules/frontend-rules.md +++ /dev/null @@ -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 ` diff --git a/frontend/src/component/components.js b/frontend/src/component/components.js index e14b30310..59023c6a8 100644 --- a/frontend/src/component/components.js +++ b/frontend/src/component/components.js @@ -6,7 +6,7 @@ import PUpdate from "component/update.vue"; import PLoading from "component/loading.vue"; import PLoadingBar from "component/loading-bar.vue"; import PLightboxMenu from "component/lightbox/menu.vue"; -import PLightboxSidebar from "component/lightbox/sidebar.vue"; +import PSidebarInfo from "component/sidebar/info.vue"; import PMap from "component/map.vue"; import PLightbox from "component/lightbox.vue"; @@ -68,9 +68,6 @@ import PPhotoArchiveDialog from "component/photo/archive/dialog.vue"; import PPhotoAlbumDialog from "component/photo/album/dialog.vue"; import PPhotoEditDialog from "component/photo/edit/dialog.vue"; -// Meta. -import PMetaFaceMarkers from "component/meta/face/markers.vue"; - // Upload. import PUploadDialog from "component/upload/dialog.vue"; @@ -89,7 +86,7 @@ export function install(app) { app.component("PLoading", PLoading); app.component("PLoadingBar", PLoadingBar); app.component("PLightboxMenu", PLightboxMenu); - app.component("PLightboxSidebar", PLightboxSidebar); + app.component("PSidebarInfo", PSidebarInfo); app.component("PMap", PMap); app.component("PLightbox", PLightbox); @@ -137,9 +134,6 @@ export function install(app) { app.component("PPhotoArchiveDialog", PPhotoArchiveDialog); app.component("PPhotoAlbumDialog", PPhotoAlbumDialog); app.component("PPhotoEditDialog", PPhotoEditDialog); - - app.component("PMetaFaceMarkers", PMetaFaceMarkers); - app.component("PUploadDialog", PUploadDialog); app.component("PServiceAdd", PServiceAdd); diff --git a/frontend/src/component/confirm/dialog.vue b/frontend/src/component/confirm/dialog.vue index 3c9f3d835..ea74addf2 100644 --- a/frontend/src/component/confirm/dialog.vue +++ b/frontend/src/component/confirm/dialog.vue @@ -15,14 +15,14 @@ > - +
{{ text ? text : $gettext(`Are you sure?`) }}
{{ $gettext(`Cancel`) }} - + {{ action ? action : $gettext(`Yes`) }} @@ -53,10 +53,6 @@ export default { type: String, default: "", }, - confirmColor: { - type: String, - default: "highlight", - }, }, emits: ["close", "confirm"], data() { diff --git a/frontend/src/component/confirm/sponsor.vue b/frontend/src/component/confirm/sponsor.vue index de106e59b..42fd42bef 100644 --- a/frontend/src/component/confirm/sponsor.vue +++ b/frontend/src/component/confirm/sponsor.vue @@ -58,7 +58,6 @@ export default { default: false, }, }, - emits: ["close"], data() { return { links, diff --git a/frontend/src/component/defaults.js b/frontend/src/component/defaults.js index 0fe136e3d..3e63e64a2 100644 --- a/frontend/src/component/defaults.js +++ b/frontend/src/component/defaults.js @@ -1,5 +1,3 @@ -import $util from "common/util"; - export default { global: { ripple: false, @@ -85,12 +83,6 @@ export default { validateOn: "invalid-input", hideDetails: "auto", }, - VTooltip: { - openDelay: 0, - closeDelay: 0, - disabled: $util.isMobile(), - transition: false, - }, VMenu: { origin: "auto", location: "bottom end", @@ -184,8 +176,7 @@ export default { ripple: false, }, VDataTable: { - // Forwarded to the footer only as the "items per page" select's active-item color (not the table). - color: "surface-variant", + color: "background", itemsPerPage: -1, hover: true, }, diff --git a/frontend/src/component/file/clipboard.vue b/frontend/src/component/file/clipboard.vue index 03a330e00..a13a0bacf 100644 --- a/frontend/src/component/file/clipboard.vue +++ b/frontend/src/component/file/clipboard.vue @@ -120,7 +120,7 @@ export default { Promise.all(uniqueAlbumUids.map((uid) => $api.post(`albums/${uid}/photos`, body))) .then(() => this.onAdded()) - .catch(() => { + .catch((error) => { $notify.error(this.$gettext("Some albums could not be updated")); }); }, diff --git a/frontend/src/component/file/delete/dialog.vue b/frontend/src/component/file/delete/dialog.vue index fb524a7d6..494ef0038 100644 --- a/frontend/src/component/file/delete/dialog.vue +++ b/frontend/src/component/file/delete/dialog.vue @@ -34,7 +34,6 @@ export default { default: false, }, }, - emits: ["close", "confirm"], data() { return {}; }, diff --git a/frontend/src/component/input/chip-selector.vue b/frontend/src/component/input/chip-selector.vue index f20a80d8d..2dd8a6d99 100644 --- a/frontend/src/component/input/chip-selector.vue +++ b/frontend/src/component/input/chip-selector.vue @@ -6,6 +6,7 @@ :key="item.value || item.title" :text="getChipTooltip(item)" location="top" + :disabled="$vuetify?.display?.mobile === true" > @@ -50,6 +51,7 @@ class="chip-selector__input" @click:control="focusInput" @keydown.enter.prevent="onEnter" + @blur="addNewItem" @update:model-value="onComboboxChange" @update:menu="onMenuUpdate" > @@ -105,10 +107,6 @@ export default { type: Function, default: null, }, - maxLength: { - type: Number, - default: 0, - }, }, emits: ["update:items"], data() { @@ -144,7 +142,7 @@ export default { if (typeof this.normalizeTitleForCompare === "function") { try { return this.normalizeTitleForCompare(input); - } catch { + } catch (e) { return input.toLowerCase(); } } @@ -160,28 +158,22 @@ export default { } if (item.action === "add") { - classes.push(item.mixed ? `${baseClass}--add-mixed` : `${baseClass}--add`); + classes.push(item.mixed ? `${baseClass}--green-light` : `${baseClass}--green`); } else if (item.action === "remove") { - classes.push(item.mixed ? `${baseClass}--remove-mixed` : `${baseClass}--remove`); + classes.push(item.mixed ? `${baseClass}--red-light` : `${baseClass}--red`); } else if (item.mixed) { - classes.push(`${baseClass}--default-mixed`); + classes.push(`${baseClass}--gray-light`); } else { - classes.push(`${baseClass}--default`); + classes.push(`${baseClass}--gray`); } return classes; }, getChipIcon(item) { - if (item.action === "add") { - return "mdi-plus"; - } - if (item.action === "remove") { - return "mdi-minus"; - } - if (item.mixed) { - return "mdi-circle-half-full"; - } + if (item.action === "add") return "mdi-plus"; + if (item.action === "remove") return "mdi-minus"; + if (item.mixed) return "mdi-circle-half-full"; return null; }, @@ -197,9 +189,7 @@ export default { }, handleChipClick(item) { - if (this.loading || this.disabled) { - return; - } + if (this.loading || this.disabled) return; let newAction; @@ -266,28 +256,14 @@ export default { return; } - if (!title) { - return; - } - - // Block the create path when the typed title exceeds the configured cap — - // otherwise the backend setter clips with an ellipsis and the user is - // shown a green success against a renamed entity they didn't intend. - if (this.maxLength > 0 && title.length > this.maxLength) { - this.$notify.error(this.$gettext("%{s} is too long", { s: this.$gettext("Name") })); - return; - } + if (!title) return; let resolvedApplied = false; if (typeof this.resolveItemFromText === "function") { const resolved = this.resolveItemFromText(title); if (resolved && typeof resolved === "object") { - if (resolved.title) { - title = resolved.title; - } - if (resolved.value) { - value = resolved.value; - } + if (resolved.title) title = resolved.title; + if (resolved.value) value = resolved.value; resolvedApplied = true; } } diff --git a/frontend/src/component/label/clipboard.vue b/frontend/src/component/label/clipboard.vue index 8ba81c07f..bcb5a2c92 100644 --- a/frontend/src/component/label/clipboard.vue +++ b/frontend/src/component/label/clipboard.vue @@ -111,7 +111,7 @@ export default { Promise.all(uniqueAlbumUids.map((uid) => $api.post(`albums/${uid}/photos`, body))) .then(() => this.onAdded()) - .catch(() => { + .catch((error) => { $notify.error(this.$gettext("Some albums could not be updated")); }); }, diff --git a/frontend/src/component/label/delete/dialog.vue b/frontend/src/component/label/delete/dialog.vue index b0e2c1408..97bf7104c 100644 --- a/frontend/src/component/label/delete/dialog.vue +++ b/frontend/src/component/label/delete/dialog.vue @@ -34,7 +34,6 @@ export default { default: false, }, }, - emits: ["close", "confirm"], data() { return {}; }, diff --git a/frontend/src/component/label/edit/dialog.vue b/frontend/src/component/label/edit/dialog.vue index 3f26ba2d6..fd40c055a 100644 --- a/frontend/src/component/label/edit/dialog.vue +++ b/frontend/src/component/label/edit/dialog.vue @@ -13,21 +13,18 @@ > - - - {{ $gettext(`Edit %{s}`, { s: model.modelName() }) }} - - - mdi-close - - + + mdi-label +
{{ $gettext(`Edit %{s}`, { s: model.modelName() }) }}
+
diff --git a/frontend/src/component/lightbox/sidebar/toolbar.vue b/frontend/src/component/lightbox/sidebar/toolbar.vue deleted file mode 100644 index b9f4d497e..000000000 --- a/frontend/src/component/lightbox/sidebar/toolbar.vue +++ /dev/null @@ -1,71 +0,0 @@ - - - diff --git a/frontend/src/component/loading-bar.vue b/frontend/src/component/loading-bar.vue index c24f646b7..891ee6c51 100644 --- a/frontend/src/component/loading-bar.vue +++ b/frontend/src/component/loading-bar.vue @@ -168,7 +168,7 @@ export default { }, methods: { - beforeEnter() { + beforeEnter(el) { this.opacity = 0; this.progress = 0; this.width = 0; @@ -179,7 +179,7 @@ export default { done(); }, - afterEnter() { + afterEnter(el) { this._runStart(); }, diff --git a/frontend/src/component/meta/location/dialog.vue b/frontend/src/component/location/dialog.vue similarity index 92% rename from frontend/src/component/meta/location/dialog.vue rename to frontend/src/component/location/dialog.vue index c48f35602..2ebb00b46 100644 --- a/frontend/src/component/meta/location/dialog.vue +++ b/frontend/src/component/location/dialog.vue @@ -7,20 +7,24 @@ persistent scrim scrollable - class="p-meta-location-dialog" - @keydown.esc.exact.stop="close" + class="p-location-dialog" + @keydown.esc.exact="close" @after-enter="afterEnter" @after-leave="afterLeave" > - + + + mdi-close + {{ $gettext("Adjust Location") }} - - mdi-close - + + mdi-map-marker +
{{ $gettext("Adjust Location") }}
+
@@ -65,9 +69,8 @@ clearable autocomplete="off" no-filter - :menu-icon="null" + menu-icon="" :menu-props="{ maxHeight: 300 }" - :list-props="{ density: 'compact' }" @update:search="onSearchQueryChange" @update:model-value="onPlaceSelected" @click:clear="clearSearch" @@ -75,12 +78,12 @@ @@ -98,7 +101,7 @@
- + >
@@ -115,10 +118,9 @@ {{ $gettext("Cancel") }} diff --git a/frontend/src/component/meta/datetime/dialog.vue b/frontend/src/component/meta/datetime/dialog.vue deleted file mode 100644 index 4106308cd..000000000 --- a/frontend/src/component/meta/datetime/dialog.vue +++ /dev/null @@ -1,331 +0,0 @@ - - - diff --git a/frontend/src/component/meta/face/markers.vue b/frontend/src/component/meta/face/markers.vue deleted file mode 100644 index 70e9b038b..000000000 --- a/frontend/src/component/meta/face/markers.vue +++ /dev/null @@ -1,1086 +0,0 @@ - - - diff --git a/frontend/src/component/navigation.vue b/frontend/src/component/navigation.vue index 04ba2a24c..960b14980 100644 --- a/frontend/src/component/navigation.vue +++ b/frontend/src/component/navigation.vue @@ -550,20 +550,6 @@ - - - {{ $gettext(`Services`) }} - - - {{ $gettext(`License`) }} @@ -655,20 +641,18 @@
@@ -680,7 +664,7 @@