Commit Graph

1091 Commits

Author SHA1 Message Date
Paul Nothaft 4b4ecfdf71 fix(header): hide language name on mobile to free the title (#523)
@Rekoo-PS reported the LanguageSelector pushing into the company-name
title on narrow viewports — the button always rendered
Globe + flag + full language name (~120px), and on mobile that pinched
the left-side title cluster in AdminHeader.

Wrap the name in `hidden sm:inline` so <sm the button collapses to
just Globe + flag, matching the existing "hidden xl:block" pattern
on the date display in the same header. Self-explanatory at icon-only
width (users see their current flag and a globe), and the dropdown
still shows full names when opened. Title/aria-label keep the name
discoverable for screen readers + tooltip hover on the icon-only state.

Refs: #523
2026-05-18 23:19:42 +02:00
Paul Nothaft b960639035 fix(og): brandable static title + wider crawler UA coverage (#521)
@Rekoo-PS reported that gallery URLs sent via the WhatsApp Business
API render an unbranded "PicPeak - Photo Sharing Platform" preview
even though manual link sends from the WhatsApp app pick up the
per-event rich preview correctly. Two root causes, two fixes:

1. WhatsApp Business and 3rd-party preview services (Twilio,
   LinkPreview.net, etc.) don't always crawl with the recognisable
   "WhatsApp/X.Y.Z" UA we matched in nginx + galleryOgService.
   Extend the regex (both copies) to also catch WhatsAppBot, wa-bot,
   LinkPreview, and Slack-ImgProxy.

2. Even with broader UA coverage, some senders cache metadata with
   no UA at all and fetch the static SPA shell. That shell's
   <title> was hard-coded to "PicPeak - Photo Sharing Platform" —
   embarrassingly generic for any self-hosted brand. Switch to
   Vite's %VITE_DEFAULT_TITLE% / %VITE_DEFAULT_DESCRIPTION% HTML
   substitution so self-hosters can bake their brand into the
   fallback at build time. Defaults stay "PicPeak" so the upstream
   image doesn't change behaviour for anyone.

The per-event rich preview path (handleGalleryOgRequest, fired on
matched crawler UAs) is unchanged — this only improves the fallback
for unrecognised UAs and for the SPA-shell title that humans see in
their browser tab.

Adds a vite.config plugin to provide the defaults when env vars
aren't set, so unsubstituted "%VITE_..." literals never reach the
built HTML. Adds .env.example entries explaining the override.

Tests: extend galleryOgService.shareImage.test.js with an
isSocialCrawler suite that pins every documented UA (incl. the new
ones) plus three browser UAs (negative) and null/empty edge cases.
Verified locally: `vite build` with VITE_DEFAULT_TITLE="MyBrand"
produces <title>MyBrand</title> + og:title="MyBrand"; without the
env var falls back to "PicPeak".

Refs: #521
2026-05-18 22:45:00 +02:00
Paul Nothaft 3465b55abc feat(events): default Guest Feedback ON via admin setting (#520)
@Rekoo-PS asked for an admin-level switch so new events can have Guest
Feedback enabled out of the box instead of toggling it on every time.
Mirrors the existing event_default_require_password pattern (#317) —
same shape end-to-end, same set of five files.

- publicSettings.js: whitelist + expose event_default_feedback_enabled
  (defaults to false to match the prior hard-coded form default; no
  behaviour change for existing installs until an admin flips it).
- adminEvents.js: rename `feedback_enabled = false` destructure to
  `feedback_enabled: feedbackEnabledInput` so we can distinguish
  "omitted" from "explicit false", then resolve the default from the
  setting only when the caller omitted it — identical to the
  require_password handling a few lines above.
- Frontend EventSettings type + state + loader: new boolean,
  default false.
- EventsTab: toggle UI right under "Require password by default".
- CreateEventPage: one-shot useEffect that seeds
  feedback_settings.feedback_enabled from the public setting on first
  load (mirrors the require_password seed effect right above it).
  Sub-toggles (likes / ratings / comments) keep their hard-coded
  true defaults so flipping the master setting immediately gives
  sensible behaviour without a second admin setting to manage.

Refs: #520
2026-05-18 21:26:45 +02:00
Paul Nothaft d44e1adba7 fix(lightbox): hide comments toggle when allow_comments=false (#518)
@Rekoo-PS reported the MessageSquare comment button stayed visible in
the lightbox toolbar even when guest comments were disabled. Same
class of bug as #513 (per-photo Like button missing the master
gate) but on a different control.

The Like and Rating buttons in the lightbox toolbar gate correctly:
  feedbackEnabled && feedbackSettings?.allow_likes
  feedbackEnabled && feedbackSettings?.allow_ratings

The MessageSquare button only checked feedbackEnabled. Since likes
and ratings already have their own inline buttons in the same
toolbar, this third button is effectively the "open comments panel"
affordance — its badge counts comments, its tooltip mentions
comments. When comments are off it has nothing meaningful to do.

Add allow_comments to the local feedbackSettings type (the backend
already returns it via galleryFeedback.js:33) and gate the button on
feedbackEnabled && feedbackSettings?.allow_comments.

Refs: #518
2026-05-18 21:13:03 +02:00
Paul Nothaft 7b8ee1c148 Merge pull request #526 from the-luap/release-please--branches--beta
Build and Push Docker Images / build-backend (linux/amd64, ubuntu-latest) (push) Has been cancelled
Build and Push Docker Images / build-backend (linux/arm64, ubuntu-24.04-arm) (push) Has been cancelled
Build and Push Docker Images / merge-backend (push) Has been cancelled
Build and Push Docker Images / build-frontend (linux/amd64, ubuntu-latest) (push) Has been cancelled
Build and Push Docker Images / build-frontend (linux/arm64, ubuntu-24.04-arm) (push) Has been cancelled
Build and Push Docker Images / merge-frontend (push) Has been cancelled
Build and Push Docker Images / summary (push) Has been cancelled
chore(beta): release 3.52.1-beta.0
v3.52.1-beta.0
2026-05-18 21:04:37 +02:00
github-actions[bot] b2b46d311d chore(beta): release 3.52.1-beta.0 2026-05-18 18:59:57 +00:00
Paul Nothaft 42c5cda38c Merge pull request #519 from the-luap/fix/install-permissions-484
fix(install): self-chowning entrypoint kills fresh-install restart loop (#484)
2026-05-18 20:59:33 +02:00
Paul Nothaft 91d47590ae Merge pull request #524 from the-luap/release-please--branches--beta
Build and Push Docker Images / build-backend (linux/amd64, ubuntu-latest) (push) Has been cancelled
Build and Push Docker Images / build-backend (linux/arm64, ubuntu-24.04-arm) (push) Has been cancelled
Build and Push Docker Images / merge-backend (push) Has been cancelled
Build and Push Docker Images / build-frontend (linux/amd64, ubuntu-latest) (push) Has been cancelled
Build and Push Docker Images / build-frontend (linux/arm64, ubuntu-24.04-arm) (push) Has been cancelled
Build and Push Docker Images / merge-frontend (push) Has been cancelled
Build and Push Docker Images / summary (push) Has been cancelled
chore(beta): release 3.52.0-beta.0
v3.52.0-beta.0
2026-05-18 20:59:14 +02:00
github-actions[bot] db3f1b83ce chore(beta): release 3.52.0-beta.0 2026-05-18 18:56:50 +00:00
Paul Nothaft 2d5a2ad78a Merge pull request #500 from munin92/feat/v1-upload-category-id
feat(api/v1): accept category_id on POST /events/:id/photos
2026-05-18 20:56:17 +02:00
Paul Nothaft 763fd4593f ci(install-smoke): use BusyBox-compatible ps in node-user check
Alpine ships BusyBox ps (no -p PID, no pgrep), which failed CI on the
first run of this workflow with "ps: unrecognized option: p". Replace
the pgrep-then-ps chain with `ps -o user,comm | awk '$2=="node"'`
which works on both BusyBox (Alpine, in the container) and procps
(the GitHub runner host, though we don't use it here).
2026-05-18 10:28:29 +02:00
Marian df83b3e923 test(api/v1): cover category scoping clause + 400 response
Unit test for the v1 upload route's category lookup, requested in
the PR review. Mocks db (chainable, mirroring src/routes/__tests__/
adminAuth.test.js) plus apiTokenAuth/requireApiScope (pass-through)
and multer (stub req.file). Two cases:

1. The scoping clause: the andWhere callback applied to a knex
   builder spy produces .where({event_id: <event.id>}).orWhere(
   'is_global', true) — exactly the contract the reviewer asked
   for, exercising the OR-clause rather than just asserting the
   callback was passed.
2. Null lookup result yields 400 with "Unknown or out-of-scope
   category_id <N>".

No v1 jest scaffolding existed before, but the project-wide harness
(backend/jest.config.js + jest.setup.js) already covers the new
file via testMatch '**/__tests__/**/*.test.js'. Happy-path tests
deferred — would require stubbing fs/sharp/imageProcessor/share
linkService and several more db chains, which the reviewer was
willing to accept as a separate follow-up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 07:21:43 +00:00
Marian 92bb9e1a12 fix(api/v1): scope category lookup to event_owned or global
PR review pointed out the original lookup
  db('photo_categories').where({ id: parsedCategoryId }).first()
accepted any category id — including one that belongs to a different
event. photo_categories carries both event_id (per-event) and is_global
(see backend/migrations/legacy/004_add_categories_and_cms.js); the v1
upload route should require either match.

Not a privilege issue (apiTokenAuth.js inherits the admin's powers, no
per-event scoping), but it lets a misconfigured uploader silently file
photos under a category the target event doesn't own — and the 201 echo
includes a category_id that makes no semantic sense.

Tighten to:
  .where({ id: parsedCategoryId })
  .andWhere(function () {
    this.where({ event_id: event.id }).orWhere('is_global', true);
  })
…and update the 400 message to "Unknown or out-of-scope category_id N".

OpenAPI description already documents the intended scope.

Tests deferred to a follow-up; v1 has no jest harness today, see PR
discussion.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 07:21:43 +00:00
Marian 6901e2661e feat(api/v1): accept category_id on POST /events/:id/photos
The v1 photo upload endpoint previously ignored any caller-supplied
category and inserted photos with category_id=NULL. That meant
programmatic uploads via API tokens (e.g. a photobox sidecar) landed
in picpeak as uncategorized, forcing operators to bulk-assign category
in the admin UI after each event.

Mirror the adminPhotos.js category-handling logic on v1:
- Read optional `category_id` from the multipart form body.
- Reject unknown ids with 400 (with the id in the error) so callers
  fail fast on misconfigured envs instead of silently uncategorized
  uploads.
- Set photos.category_id on insert.
- Flip photos.type to 'collage' when the category's slug is
  collage/collages, matching adminPhotos.

Backwards-compatible: omitting category_id keeps the prior behavior
(insert with NULL category, type='individual'). OpenAPI spec + 201
response body updated to include the new field.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 07:21:43 +00:00
Paul Nothaft 1505775678 fix(install): self-chowning entrypoint kills fresh-install restart loop (#484)
The fresh-install restart loop reported by @MrGabri (and confirmed by
@AloePacci with the user:0:0 workaround) had a clear root cause:

  - Dockerfile pinned USER nodejs (UID 1001) before the entrypoint
    ran, so the existing chown branch in init-production.sh:13 was
    dead code.
  - wait-for-db.sh (the actual entrypoint, not init-production.sh)
    silently swallowed mkdir/EACCES on bind mounts with || true,
    then a downstream migration error surfaced as the visible failure.
  - Net effect on a typical Linux host where the bind-mount dir is
    owned by UID 1000: container can't write, exits non-zero,
    restarts forever with no clear error.

Switch to the standard Docker drop-privileges pattern:

  1. Install su-exec, drop `USER nodejs` from the Dockerfile —
     container now starts as root.
  2. wait-for-db.sh: if running as root, chown /app/storage,
     /app/data, /app/logs to nodejs and re-exec self via
     su-exec nodejs:nodejs. App still ends up running as UID 1001.
  3. Preflight check for non-root invocations (compose `user:`
     overrides): verify the bind mounts are actually writable
     before continuing. If not, exit 1 immediately with an
     actionable error pointing at the docs — no more silent
     restart loops.

Also:

  - Delete backend/init-production.sh. It was an orphan — no caller
    in the Dockerfile, compose, or anywhere else. Its chown logic
    looked authoritative enough that @MrGabri ran it manually trying
    to debug, which is what finally surfaced the EACCES.
  - docker-compose.yml: drop user: + PUID/PGID env. The pattern-B
    UID-matching workaround they implemented is obsolete now that
    pattern A (root-then-drop) is in place.
  - .env.example + README: drop PUID/PGID documentation.
  - Add fresh-install smoke test workflow. Boots backend + postgres
    against bind mounts owned by UID 1000 (the GitHub runner UID,
    and the common-mismatch case on Linux hosts) and verifies:
    + container reaches healthy without restart-looping
    + chown happened (dirs now owned by 1001 inside the container)
    + node runs as nodejs, not root (su-exec drop worked)
    + /health returns status:ok
    + with --user 5005:5005 + unwritable mounts, preflight exits
      loud with the expected error string

Verified locally end-to-end against a fresh Postgres + UID-501-owned
bind mount: backend reaches healthy in ~20s, chown applied, node
runs as nodejs, no restart loop. Docs in picpeak-docs cover the new
behavior + a Troubleshooting section for the install-path bugs
fixed in #484/#494/#511/#488.

Refs: #484
2026-05-17 22:29:29 +02:00
Paul Nothaft 2619049a95 Merge pull request #517 from the-luap/release-please--branches--beta
Build and Push Docker Images / build-backend (linux/amd64, ubuntu-latest) (push) Has been cancelled
Build and Push Docker Images / build-backend (linux/arm64, ubuntu-24.04-arm) (push) Has been cancelled
Build and Push Docker Images / merge-backend (push) Has been cancelled
Build and Push Docker Images / build-frontend (linux/amd64, ubuntu-latest) (push) Has been cancelled
Build and Push Docker Images / build-frontend (linux/arm64, ubuntu-24.04-arm) (push) Has been cancelled
Build and Push Docker Images / merge-frontend (push) Has been cancelled
Build and Push Docker Images / summary (push) Has been cancelled
chore(beta): release 3.51.5-beta.0
v3.51.5-beta.0
2026-05-17 09:29:01 +02:00
github-actions[bot] acb387f69c chore(beta): release 3.51.5-beta.0 2026-05-17 07:28:47 +00:00
Paul Nothaft ebc7da21be Merge pull request #503 from filpgame/fix/email-language-json-parse
fix(email): parse JSON-encoded language setting before using as locale
2026-05-17 09:28:25 +02:00
Paul Nothaft afcdf0d389 Merge pull request #516 from the-luap/release-please--branches--beta
Build and Push Docker Images / build-backend (linux/amd64, ubuntu-latest) (push) Has been cancelled
Build and Push Docker Images / build-backend (linux/arm64, ubuntu-24.04-arm) (push) Has been cancelled
Build and Push Docker Images / merge-backend (push) Has been cancelled
Build and Push Docker Images / build-frontend (linux/amd64, ubuntu-latest) (push) Has been cancelled
Build and Push Docker Images / build-frontend (linux/arm64, ubuntu-24.04-arm) (push) Has been cancelled
Build and Push Docker Images / merge-frontend (push) Has been cancelled
Build and Push Docker Images / summary (push) Has been cancelled
chore(beta): release 3.51.4-beta.0
v3.51.4-beta.0
2026-05-17 09:27:19 +02:00
github-actions[bot] 4da6726e57 chore(beta): release 3.51.4-beta.0 2026-05-17 07:26:57 +00:00
Paul Nothaft a747eb351d Merge pull request #502 from filpgame/fix/category-slug-diacritics
fix(categories): strip diacritics from auto-generated slugs
2026-05-17 09:26:39 +02:00
Paul Nothaft 4fc07d9282 Merge pull request #515 from the-luap/release-please--branches--beta
Build and Push Docker Images / build-backend (linux/amd64, ubuntu-latest) (push) Has been cancelled
Build and Push Docker Images / build-backend (linux/arm64, ubuntu-24.04-arm) (push) Has been cancelled
Build and Push Docker Images / merge-backend (push) Has been cancelled
Build and Push Docker Images / build-frontend (linux/amd64, ubuntu-latest) (push) Has been cancelled
Build and Push Docker Images / build-frontend (linux/arm64, ubuntu-24.04-arm) (push) Has been cancelled
Build and Push Docker Images / merge-frontend (push) Has been cancelled
Build and Push Docker Images / summary (push) Has been cancelled
chore(beta): release 3.51.3-beta.0
v3.51.3-beta.0
2026-05-17 01:20:56 +02:00
github-actions[bot] 941453fd7f chore(beta): release 3.51.3-beta.0 2026-05-16 23:19:50 +00:00
Paul Nothaft 482e91bbf8 Merge pull request #501 from filpgame/fix/settings-page-language-reset
fix(i18n): settings page resets UI language to server default
2026-05-17 01:19:27 +02:00
Paul Nothaft aa8cca165d Merge pull request #514 from the-luap/release-please--branches--beta
Build and Push Docker Images / build-backend (linux/amd64, ubuntu-latest) (push) Has been cancelled
Build and Push Docker Images / build-backend (linux/arm64, ubuntu-24.04-arm) (push) Has been cancelled
Build and Push Docker Images / merge-backend (push) Has been cancelled
Build and Push Docker Images / build-frontend (linux/amd64, ubuntu-latest) (push) Has been cancelled
Build and Push Docker Images / build-frontend (linux/arm64, ubuntu-24.04-arm) (push) Has been cancelled
Build and Push Docker Images / merge-frontend (push) Has been cancelled
Build and Push Docker Images / summary (push) Has been cancelled
chore(beta): release 3.51.2-beta.0
v3.51.2-beta.0
2026-05-17 01:02:48 +02:00
github-actions[bot] 5a9ca4ad57 chore(beta): release 3.51.2-beta.0 2026-05-16 23:02:10 +00:00
Paul Nothaft aac60fa895 Merge pull request #513 from the-luap/fix/bug-batch
fix/feat: bug batch — drag-drop, lightbox, likes, downloads, upload, i18n (#504-510)
2026-05-17 01:01:49 +02:00
Paul Nothaft 51890e1aa5 fix(i18n): drive customer "Preferred language" select from SUPPORTED_LANGUAGES (#510)
Audit follow-up to the Spanish-locale commit: `CustomerDetailPage` had
a hardcoded `<option>` list for the customer's preferred-language
selector — 5 entries (en/de/nl/pt/ru) that were missing both fr (an
existing gap) and es (the new one). Every other language selector in
the frontend (the navbar `LanguageSelector`, the `GeneralTab` default-
language dropdown, the `EmailConfigPage` per-language tabs) already
reads from the shared `SUPPORTED_LANGUAGES` constant, so adding es
there was enough for those. This one had drifted.

Now mapped from `SUPPORTED_LANGUAGES` so future locales only need to
touch one place.
2026-05-17 00:57:03 +02:00
Paul Nothaft 1e7806961f Merge pull request #512 from the-luap/release-please--branches--beta
Build and Push Docker Images / build-backend (linux/amd64, ubuntu-latest) (push) Has been cancelled
Build and Push Docker Images / build-backend (linux/arm64, ubuntu-24.04-arm) (push) Has been cancelled
Build and Push Docker Images / merge-backend (push) Has been cancelled
Build and Push Docker Images / build-frontend (linux/amd64, ubuntu-latest) (push) Has been cancelled
Build and Push Docker Images / build-frontend (linux/arm64, ubuntu-24.04-arm) (push) Has been cancelled
Build and Push Docker Images / merge-frontend (push) Has been cancelled
Build and Push Docker Images / summary (push) Has been cancelled
chore(beta): release 3.51.1-beta.0
v3.51.1-beta.0
2026-05-17 00:53:34 +02:00
github-actions[bot] 2e828fcf9f chore(beta): release 3.51.1-beta.0 2026-05-16 22:53:14 +00:00
Paul Nothaft 99e60a2433 Merge pull request #511 from the-luap/fix/install-postgres-log-noise
fix(install): silence clean-install postgres log noise (#484)
2026-05-17 00:52:48 +02:00
Paul Nothaft 061712ebf1 feat(i18n): add Spanish (es) locale (#510)
Contributed by @AloePacci on issue #510. Drops their es.json into the
existing locale set, registers Spanish in the language selector with a
flag SVG matching the inline style of the other six locales, and
extends the email pipeline so es-language guests receive a localised
email subject/body where available.

Coverage:
- frontend/src/i18n/locales/es.json — 2132 translated keys. ~824
  EN keys are not yet covered; i18next's `fallbackLng: 'en'` handles
  those at runtime so the UI never renders a missing key. fr/nl/pt/ru
  have a similar (smaller) gap and ship the same way.
- LanguageSelector.tsx — added the ESFlag inline SVG (red/yellow/red
  horizontal bands, official #AA151B + #F1BF00; no coat of arms to
  stay consistent with the other simple flag components) and a new
  entry in SUPPORTED_LANGUAGES.
- emailProcessor.js — added .es to the domain-language heuristic, and
  an `es:` row to the three inline-translated snippets
  (passwordSecurityI18n / noPasswordI18n / passwordSetAtCreationI18n).
- 106_seed_es_email_template_translations.js (new) — idempotent
  seeder for the four customer-facing templates AloePacci translated:
  gallery_created, expiration_warning, gallery_expired, archive_complete.
  Mirrors the pattern from 099. Template keys without an `es` row fall
  back to `en` via the existing resolution chain in
  emailProcessor.processTemplate — no functional gap, just untranslated
  copy until someone fills them in.

What I deliberately did NOT take from the contribution: the proposed
in-place edit of migration 075 (history mutation — won't reseed for
existing installs anyway) and the whitespace/`gallery_list_html`-drop
churn in emailProcessor.js (would have regressed the #354 follow-up).
The semantic additions from those files are preserved via 106 and the
targeted edits above.
2026-05-17 00:50:51 +02:00
Paul Nothaft 98f3c3df41 fix(upload): restore configurable batch-size for reverse proxies (#509)
Regression of #208. PR #214 (commit 02a46e0, re-merged at 9b7495e)
shipped the configurable `general_max_upload_batch_size_mb` setting so
users behind Cloudflare Tunnel and other reverse proxies with
per-request size caps could lower the chunked-upload size below their
proxy's limit. Six days later the "Merge main into beta for
release/beta-to-main" commit (28793bb) resolved its conflict by
keeping main's older tree — which silently deleted the migration
(072), the setting input on Settings → General, the i18n strings, the
`useSettingsState` field, and the read in PhotoUpload.tsx, putting the
hardcoded 500MB chunk back. Galleries fronted by Cloudflare have
quietly been broken on batch uploads since then.

Re-applying exactly the same change set:

- `backend/migrations/core/072_add_max_upload_batch_size.js`
  recreated, with a comment pointing at the regression in case the
  same merge accident happens again.
- `frontend/src/components/admin/PhotoUpload.tsx` line 168 now reads
  the setting from query cache and falls back to 95MB (Cloudflare-safe
  headroom under 100MB).
- `useSettingsState.ts`, `GeneralTab.tsx`, `en.json`, `de.json` —
  added the field to the state type + defaults + load path + the
  Site-Configuration input.

Existing installs are safe either way:
- Ran original 072 then lost the file: migrations table still has the
  filename, so the runner skips re-applying. The setting row in
  `app_settings` is also untouched (the deletion was source-only, no
  down migration ran). Now the new code starts reading it again.
- Installed after the regression: migrations runner picks up the new
  072 normally and seeds the setting at 95.
2026-05-17 00:42:22 +02:00
Paul Nothaft 33de294d57 feat(lightbox): surface original camera filenames (#508)
Photographers running the gallery as a client-selection tool want to
map a guest's picks back to source files for retouching. The
`general_use_original_filenames_for_downloads` toggle (#493) already
does this on the download side; this extends the same toggle to the
in-lightbox view so the camera filename is visible alongside the
photo while it's being looked at.

Tied to the same toggle on purpose — one switch controls both
surfaces. Off by default; existing galleries keep showing only the
position counter.

Wiring:
- gallery.js serializes `photos[].original_filename` and surfaces the
  resolved toggle as `event.use_original_filenames` so the client can
  decide whether to render it.
- The bespoke `PhotoLightbox` renders the original filename (falling
  back to the storage filename only for pre-migration-062 uploads) in
  a muted line under the position counter, truncated to keep the
  toolbar tidy.
- `GalleryStoryLayout` mounts the same `PhotoLightbox`, so its
  rendering follows along.
- `GalleryPremiumLayout` uses `yet-another-react-lightbox` instead;
  added the Captions plugin and a `title` field on the slides so the
  same name appears as a caption when the toggle is on.

The remaining layouts feed back into the main `PhotoLightbox` via
`PhotoGridWithLayouts`, so the prop reaches them through the layout
props bag.
2026-05-17 00:35:16 +02:00
Paul Nothaft 38343e62de fix(downloads): apply original-filename toggle to individual downloads too (#507)
Follow-up to #498. The toggle reached zip downloads but single-photo
downloads still landed on disk with the renamed `event_individual_NNN.jpg`
even when the admin had flipped the setting on. Two reasons, fixed
in lockstep:

- Frontend overrode the server's Content-Disposition with a hardcoded
  `<a download="X">` attribute (`gallery.service.ts`, `photos.service.ts`)
  where X was the sanitized `photo.filename` known to the client. So
  the backend's correctly-formed `Content-Disposition` never reached
  the disk write. Added `parseContentDispositionFilename` (RFC 5987 +
  plain `filename=` fallback) and let the server name win when present.
- `secureImages.js` (enhanced/maximum protection's secure-download
  route) was missed in #498 and still emitted a hardcoded
  `filename="${photo.filename}"` regardless of the toggle. Wired it
  through `getUseOriginalFilenames` + `buildContentDisposition` so it
  matches the regular gallery download path.

Also exposed `Content-Disposition` via CORS so split (cross-origin)
frontend deployments can still read it from JavaScript. Same-origin
Docker deploys already had access; this is a defensive addition for
the split case.
2026-05-17 00:25:12 +02:00
Paul Nothaft 9d2db9a73b fix(gallery): hide Like button when guest feedback is off (#506)
Four gallery layouts were rendering the per-photo Like button without
gating on the master "Guest Feedback" toggle, so a guest still saw a
heart icon and could submit likes on events where the host had turned
feedback off. The other layouts (Grid / Justified / Masonry / Story)
already gated correctly with `feedbackEnabled && allowLikes` —
Rekoo-PS's note that "it's hidden in some themes" matches that split.

- CarouselGalleryLayout, MosaicGalleryLayout, TimelineGalleryLayout:
  the existing conditional checked only `feedbackOptions?.allowLikes`,
  missing the `feedbackEnabled` master gate. Added it inline.
- GalleryPremiumLayout: the per-card Like button rendered
  unconditionally because PhotoCard never received the allow-likes
  signal. Added an `allowLikes` prop on PhotoCardProps, plumbed
  `feedbackOptions?.allowLikes` down from the parent, and wrapped the
  button in `feedbackEnabled && allowLikes`.

The follow-up "default guest-feedback ON" request from Rekoo-PS in
the comments is a separate feature (admin > General > Event Creation
default) and out of scope for this fix.
2026-05-17 00:13:21 +02:00
Paul Nothaft d2d55098d6 fix(lightbox): align swipe-neighbour height + stop black flash on commit (#505)
Two adjacent swipe-time defects, one diagnosis each:

1. Height differed between current and neighbouring slides during a
   swipe but matched when the arrow buttons advanced the carousel.
   Cause: neighbour slides wrap their image in a div with extra `px-2`
   horizontal padding while the current slide does not. `object-contain`
   then sees a narrower container on neighbours, so wide images cap on
   width first and render shorter than the same image at the current
   position. Removed the padding so both slots share the same container
   geometry. Arrow-button navigation looked fine because it never
   showed the neighbour layout side-by-side.

2. The image flashed black for ~100–400 ms each time a swipe committed
   to the next slide. Cause: the 3-slide track has no React keys, so
   React reconciled slides by position. After commit the photo at every
   position changed (`prev → current → next` shifts left), every slot's
   `<AuthenticatedImage>` saw a new `src` prop, and its fetch effect
   restarted from the placeholder state — including the slot that was
   the user's "next" slide a moment ago and held a fully-loaded image.
   Added a stable `key` derived from `photo.id` so React MOVES existing
   DOM nodes across slots instead of refetching. 2-photo galleries are
   a key-collision edge case (`prev === next`), so they fall back to
   slot-prefixed keys to keep siblings unique; behaviour there is no
   worse than today.
2026-05-17 00:09:15 +02:00
Paul Nothaft 577c4bdf29 fix(upload): wire drag-and-drop on admin + user upload zones (#504)
The dashed-border upload area in `PhotoUpload` (admin) and
`UserPhotoUpload` (gallery user-upload) is styled and labelled as a
drop zone — every locale's `upload.clickToUpload` already reads
"Click to upload or drag and drop" or its translation — but neither
component had any `onDragOver` / `onDragEnter` / `onDragLeave` /
`onDrop` handlers. Files dropped on the zone fell through to the
browser's default behaviour (open the image in a new tab), which is
what Rekoo-PS reported.

Added native HTML5 drag-and-drop wiring on both components, plumbed
through the same filter/limit/toast pipeline used by the click path
(`addFiles` helper). Visual highlight on drag-over via an `isDragOver`
flag; the listener guards against the `dragleave` strobing that fires
on every child node. Also reset the `<input>` value after onChange so
re-picking the same file still triggers an upload — matches the
new drop-then-pick mental model.
2026-05-17 00:02:39 +02:00
Paul Nothaft 86b33d4dda fix(install): silence clean-install postgres log noise (#484)
Two latent install-time issues that emitted scary postgres ERROR lines
on every fresh start but didn't actually break anything. MrGabri flagged
them after #494 had already cleared the FK-ordering crash.

1. Migration 035 builds three `CREATE INDEX` statements against
   `backup_runs(created_at, …)`, but 029 creates the table with
   `started_at` and no `created_at`. The wrapping try/catch silently
   swallowed the resulting `column "created_at" does not exist` ERROR,
   so the migration "succeeded" without ever creating the indexes.
   Switched 035 to reference `started_at` (same chronological semantics)
   and added migration 105 to create the same indexes idempotently for
   deployments whose 035 already ran and silently failed.

2. `run-migrations-safe.js` snapshots `appliedFilenames` *before*
   `detectExistingSchema()` runs. When `detectExistingSchema()` inserts a
   row for e.g. `004_add_categories_and_cms.js` (because its tables exist
   from a partially-completed prior install), the subsequent migration
   loop still doesn't know about that insert, attempts the legacy
   migration anyway, and its transaction-internal
   `insert into migrations` conflicts with the row already there.
   Re-query the applied set after detectExistingSchema so the loop sees
   the corrected snapshot.

No behavioural change for healthy installs. New installs no longer log
the `column "created_at" does not exist` or `duplicate key value
violates unique constraint "migrations_filename_unique"` ERRORs.
2026-05-16 23:56:05 +02:00
filpgame f12062f1e7 fix(email): parse JSON-encoded language setting before using as locale
general_default_language is stored as a JSON string (e.g. "\"pt\"").
getRecipientLanguage() returned the raw value including quotes, causing
the translation lookup to miss every match and fall back to English.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 03:31:15 -03:00
filpgame 848430e72b fix(categories): strip diacritics from auto-generated slugs
Accented chars (ã, ç, é, etc.) were silently dropped by the slug
regex because \w only matches ASCII. NFD decomposition + combining
mark removal converts them to ASCII equivalents instead.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 02:23:03 -03:00
filpgame 165ebce8d1 fix: settings page resets UI language to server default
When navigating to the Settings page, useSettingsState called
i18n.changeLanguage() with the server-stored general_default_language
value on every settings query resolution. This caused the admin UI
language to reset to the server default (e.g. "en") regardless of the
language the user had selected via the LanguageSelector.

The general_default_language setting is intended as the default for
public galleries, not for controlling the admin UI language. The admin
UI language is already persisted via localStorage through
i18next-browser-languagedetector and should not be overridden by server
settings.

Remove the i18n.changeLanguage() call from the useEffect that
initialises settings state from the API response.
2026-05-16 01:14:57 -03:00
Paul Nothaft 3a490844e1 Merge pull request #499 from the-luap/release-please--branches--beta
Build and Push Docker Images / build-backend (linux/amd64, ubuntu-latest) (push) Has been cancelled
Build and Push Docker Images / build-backend (linux/arm64, ubuntu-24.04-arm) (push) Has been cancelled
Build and Push Docker Images / merge-backend (push) Has been cancelled
Build and Push Docker Images / build-frontend (linux/amd64, ubuntu-latest) (push) Has been cancelled
Build and Push Docker Images / build-frontend (linux/arm64, ubuntu-24.04-arm) (push) Has been cancelled
Build and Push Docker Images / merge-frontend (push) Has been cancelled
Build and Push Docker Images / summary (push) Has been cancelled
chore(beta): release 3.51.0-beta.0
v3.51.0-beta.0
2026-05-14 23:30:58 +02:00
github-actions[bot] 72c2b5c796 chore(beta): release 3.51.0-beta.0 2026-05-14 21:25:07 +00:00
Paul Nothaft 826e43ebac Merge pull request #498 from the-luap/feat/lightbox-preview-tier-492
feat(downloads): preserve original camera filenames on download (opt-in) (#493)
2026-05-14 23:24:41 +02:00
Paul Nothaft 019b0c0301 Merge pull request #497 from the-luap/release-please--branches--beta
Build and Push Docker Images / build-backend (linux/amd64, ubuntu-latest) (push) Has been cancelled
Build and Push Docker Images / build-backend (linux/arm64, ubuntu-24.04-arm) (push) Has been cancelled
Build and Push Docker Images / merge-backend (push) Has been cancelled
Build and Push Docker Images / build-frontend (linux/amd64, ubuntu-latest) (push) Has been cancelled
Build and Push Docker Images / build-frontend (linux/arm64, ubuntu-24.04-arm) (push) Has been cancelled
Build and Push Docker Images / merge-frontend (push) Has been cancelled
Build and Push Docker Images / summary (push) Has been cancelled
chore(beta): release 3.50.0-beta.0
v3.50.0-beta.0
2026-05-14 23:22:44 +02:00
Paul Nothaft 7eeef2ba98 feat(downloads): preserve original camera filenames on download (opt-in) (#493)
New Settings → General toggle `Use original filenames on download` (off by
default). When on, single-photo downloads, bulk/selection zips, and per-event
archive zips surface `photos.original_filename` instead of the sanitized
storage filename. Storage paths are unchanged.

- Content-Disposition uses RFC 5987 (`filename=` ASCII + `filename*=UTF-8''…`)
  so unicode camera filenames survive while header-injection bytes are stripped.
- Zip entries are deduplicated with a deterministic `_1` / `_2` suffix on
  collision (folder structure preserved in archive zips).
- Pre-generated download-all zips and the in-memory setting cache are
  invalidated when the toggle flips so the next download rebuilds with the
  new names.
- Falls back to the storage filename whenever `original_filename` is null
  (legacy uploads predating migration 062).
2026-05-14 23:11:00 +02:00
github-actions[bot] 365582e678 chore(beta): release 3.50.0-beta.0 2026-05-14 20:44:57 +00:00
Paul Nothaft 3083c748b9 Merge pull request #496 from the-luap/feat/lightbox-preview-tier-492
feat(lightbox): medium-resolution preview tier (#492)
2026-05-14 22:44:35 +02:00
Paul Nothaft 61f1d13210 feat(lightbox): medium-resolution preview tier (#492)
Adds an opt-in lightbox preview tier so guests open photos against an
aspect-preserved ~1920px JPEG (~200–500 KB) instead of the full original
(often 5–12 MB). Originals are still served on Download.

Backend:
  - imageProcessor: generatePreviewImage / isPreviewValid / ensurePreviewImage
    using fit:'inside' + withoutEnlargement (longEdge 1920, q85, mozjpeg)
  - migration 104: photos.preview_path + lightbox_preview_enabled setting
    (off by default, JSON-stringified for SQLite/Postgres parity)
  - GET /api/gallery/:slug/preview/:photoId — gallery-auth, lazy generation,
    ETag based on mtime+photoId+watermarkHash
  - preview_url surfaced in the photo response only when the toggle is on
  - admin /thumbnails/regenerate-previews mirrors regenerate-thumbnails,
    skipping videos
  - backup walk + archive cleanup + photo-delete now include previews/

Frontend:
  - PhotoLightbox uses photo.preview_url ?? photo.url (null-safe fallback)
  - ThumbnailsTab gets a Lightbox Preview Tier card: opt-in toggle +
    Regenerate All Previews button (gated until the toggle is on)
  - en/de locale strings; nl/pt/ru/fr fall back to en

Tested end-to-end: 11 MB / 4000×3000 source → 985 KB / 1920×1440 preview,
381 ms first call, 7 ms cached, ~91% byte reduction.
2026-05-14 22:30:39 +02:00