Commit Graph
2397 Commits
Author SHA1 Message Date
Paul Nothaft 46bc894d91 docs: add Buy Me a Coffee badge + Support section
Adds a yellow Buy Me a Coffee badge to the header alongside the existing
License/Docker/Node/React badges, plus a small "Support the Project"
section above Acknowledgments with the standard BMC button image. Also
adds a link in the inline nav row at the top of the README so first-time
visitors can find it.

Link: https://buymeacoffee.com/theluap

Lightweight, opt-in support — explicitly notes that starring, sharing,
filing good bug reports, and opening PRs are equally welcome ways to
help if money isn't in the budget.
2026-04-27 22:38:00 +02:00
Paul Nothaft 038e84cae7 fix: dedupe parallel admin 401 redirects to /admin/login
Visiting /admin/dashboard while logged out caused a navigation storm:
the dashboard fires ~7 /api/admin/* queries on mount, each returns 401,
each axios interceptor call did `window.location.href = '/admin/login'`.

The path-based guard `currentPath.includes('/admin/login')` reads
`location.pathname` *synchronously* — but `location.href = …` is async,
so all 7 parallel handlers saw the still-old pathname and each fired a
fresh navigation. The browser logged 6+ ERR_ABORTED entries and the user
saw a flicker storm. Same shape would bite any admin page that fans out
queries on mount.

Add a module-level `adminLoginRedirectPending` flag set the moment we
kick off the first redirect; subsequent 401s in the same tick see it
and skip. Single navigation, clean transition to login.

Smoke spec 10-admin-redirect-loop locks the regression in by sampling
the URL across 5 ticks — if any tick lands somewhere other than
/admin/login, the spec fails.
2026-04-27 22:38:00 +02:00
Paul Nothaft 2eead52319 fix: theme picker buttons no longer submit the parent form (#326)
Every <button> inside ThemeCustomizerEnhanced was bare — no `type`
attribute, defaulting to `type="submit"`. Inside CreateEventPage's
<form onSubmit={handleSubmit}>, that turned every theme/layout/header/
divider/control/colour-mode/CSS-template click into a form submission.

When the form was empty, validation killed the submit silently — that
showed up earlier as #317.2 ("theme picker unclickable").

When the form was filled (event_name set, etc.), validation passed,
`createMutation.mutate(payload)` ran, and the user was navigated to a
freshly-created event they never asked for — #326's reported symptom.

Fix: add `type="button"` to all 9 unmarked <button>s in the customizer.
Also covered by smoke spec 09-create-event-no-instant-submit which fills
the form, clicks Modern Masonry, and asserts the URL stays on
/admin/events/new and the events count is unchanged.
2026-04-27 22:38:00 +02:00
Paul Nothaft 808b15bafb feat: public v1 API + token management + OpenAPI docs (#322)
Adds a long-lived bearer-token mechanism + scoped REST surface designed
for n8n-style automation: create a gallery, upload photos, fetch the
share URL — all via documented HTTPS endpoints instead of poking at the
admin UI's internal routes.

API
- Migration 081 adds `api_tokens` (hashed_token, scopes, owner FK,
  last_used/expires/revoked timestamps).
- New apiTokenAuth middleware: parses `Authorization: Bearer pp_live_…`,
  resolves to the owner admin user, attaches `req.admin` so existing
  permission decorators (events.create etc.) still work. Token-level
  scope check (read/write/admin) layers on top as defence in depth —
  a leaked read-only token cannot mutate even if its owner is super_admin.
- adminApiTokens route exposes list/create/revoke for admins (cookie-
  authed). Plaintext token is returned exactly once on creation.
- v1 surface mounted at /api/v1: POST/GET /events, GET /events/:id,
  POST /events/:id/photos (multipart, single file), GET
  /events/:id/share-link. Each endpoint annotated with @openapi JSDoc.

Documentation
- swagger-jsdoc + swagger-ui-express produce a live spec at
  /api/openapi.json and a Swagger UI at /api/docs (admin-gated).
- backend/scripts/generate-openapi.js writes docs/openapi.{json,yaml}
  to the repo so the spec is versioned.
- scripts/sync-api-docs.sh runs in pre-push: regenerates the spec and
  copies it into the picpeak-docs Nextra site at app/api/. Writes only,
  never commits or pushes the docs repo (PUSH_SKIP_DOCS=1 to bypass).

Frontend
- New Settings → API Tokens tab: generate, list, revoke. Plaintext
  tokens are shown once with a copy-to-clipboard control.
2026-04-27 22:38:00 +02:00
Paul Nothaft be6cb28c80 feat: optional customer phone field gated by global toggle (#322)
Adds a `customer_phone` column on events plus an `event_phone_field_enabled`
admin setting (default off) that surfaces the input in the create-event
and event-detail forms. Designed for downstream automation tooling — once
exposed via the upcoming public API, n8n / similar can pick it up to
deliver gallery links over WhatsApp, SMS, etc.

- Migration 080 adds the column + seeds the setting as false. Existing
  deployments see no UI change unless the admin opts in via
  Settings → Events.
- Backend strips the field server-side when the toggle is off (defence
  in depth against form bypass).
- Frontend renders the input only when the public-settings flag is true;
  always optional even then.
- publicSettings + EventSettings types extended; CreateEventPage and
  EventDetailsPage wired to read the toggle and submit the value.
2026-04-27 22:38:00 +02:00
Paul Nothaft 4f77905b87 feat: customisable 404 + gallery-not-found pages via CMS (#324)
The 404 catch-all and the "gallery not found" branches in GalleryPage
were hard-coded English strings on a default-themed background — the
one place where a white-labelled deployment leaked the PicPeak default
look. Pluggable now via the existing CMS Pages mechanism.

Backend:
- Seed two new default CMS pages: `not-found` and `gallery-not-found`,
  with sensible English/German copy admins can edit in /admin/cms.
- Add `cms_pages.logo_url` (nullable) for per-page logo override; online
  migration on existing deployments. Null falls back to the global
  branding logo.
- New per-page logo upload (POST /api/admin/cms/pages/:slug/logo) +
  clear endpoint (DELETE …/logo). Reuses the existing /uploads/logos
  storage location with a `cms-<slug>-` filename prefix.
- adminCMS PUT now accepts logo_url; publicCMS GET returns it.

Frontend:
- New <CMSContentBlock slug fallback> component renders the CMS page in
  the standard branded shell (logo precedence: page → branding → bundled
  default), with DOMPurified content and footer/legal links.
- App.tsx: `path="*"` catch-all routes through CMSContentBlock("not-found").
- GalleryPage: collapses the two "gallery not found" branches (invalid
  identifier + infoError archived/missing) into a single
  CMSContentBlock("gallery-not-found"), so admins can edit one source
  of truth.
- Admin CMS Page editor gains an "Upload Logo / Use site default"
  control per page; falls back to the page's own English title in the
  page list when no `legal.<slug>` translation is registered.
2026-04-27 22:38:00 +02:00
Paul Nothaft b63a8774c4 fix: theme-preset match loop ignores extra fields like logoUrl (#323)
The "which preset does this saved theme match?" loop in BrandingPage and
CreateEventPage was doing a full JSON.stringify equality on preset.config
vs the loaded theme. The previous #323 logo-preservation work means the
saved theme legitimately carries a `logoUrl` (and any other fields the
parent maintains), so the equality check would never match and the
preset summary fell back to "Custom Theme" / Classic Grid even when the
saved theme was structurally Dark Modern, etc.

Compare only on the preset's own keys instead. Surfaced by the new
smoke spec 07-branding-default-on-create-event which would otherwise
pass green against the broken state.
2026-04-27 22:38:00 +02:00
Paul Nothaft 793e410554 fix: floor password_changed_at when comparing against JWT iat
JWT `iat` has 1-second resolution; `password_changed_at` is stored with
sub-second precision. The previous comparison rejected tokens whose iat
fell in the same wall-clock second as a password change — e.g. a token
issued by an immediate re-login after a password reset, or by any
script-driven flow that resets and logs in in quick succession. Floor
the stored timestamp to whole seconds before comparing.

Caught while wiring up the local E2E suite: the seeder needed a
"set password_changed_at 10 s in the past" hack to avoid this race;
with the fix in place that hack is gone and the suite is naturally
deterministic.
2026-04-27 22:38:00 +02:00
Paul Nothaft 8d0fb8e157 chore: expose pid + uptime on /health for crash-detection monitors
Adds `pid` and `uptime` fields to the /health response so external monitors
(and the local E2E watchdog) can detect a silent process restart between
two checks — e.g. an unhandled rejection that crashes Node and Docker
quietly relaunches the container.

Also adds .gitignore patterns for a local-only E2E suite that lives in
tests/e2e/local/ on individual machines and is never pushed.
2026-04-27 22:38:00 +02:00
Paul Nothaft 822be9a9b2 fix: theme save without Live Preview, Branding default on new events, gallery loading flicker (#323, #321)
#323-A — Branding colour changes weren't persisting unless "Apply changes
immediately (Live Preview)" was checked. ThemeCustomizerEnhanced was
gating its `onChange` callback on `isPreviewMode`, but the parent
BrandingPage already gates global `setTheme()` on its own copy of that
flag — so the customizer's gate was double-gating and silently dropped
the new values from the parent state that Save reads from. Always
propagate `onChange`; let parents decide what's "live". Removed the now
no-op `isPreviewMode` prop and dropped the unused passers.

#323-B — Default theme set in Branding wasn't applied to new events.
CreateEventPage only inherited the event-type's recommended preset, with
'default' falling back to Classic Grid. Now reads `settings.theme_config`
on first load and uses it as the form's starting theme; the event-type
effect skips the generic 'default' so the Branding default sticks for
event types like "Other".

#321 — Visitors saw four sequential render states when opening a gallery
(full-page "Loading Gallery" → "publicly accessible — loading photos"
card → skeleton grid → real gallery). Extracted the skeleton into a
shared <GallerySkeleton/> and used it for both GalleryView's photos-
loading state and GalleryPage's gallery-info-loading + public-auto-login
phases. The "publicly accessible" interstitial is gone. Net: one
continuous skeleton from URL open until real photos render.
2026-04-27 22:38:00 +02:00
Paul Nothaft 63a6bfebce Merge pull request #320 from the-luap/release-please--branches--beta
Build and Push Docker Images / build-backend (push) Failing after 3m41s
Build and Push Docker Images / build-frontend (push) Failing after 3m40s
Build and Push Docker Images / summary (push) Successful in 2s
chore(beta): release 3.29.1-beta.0
v3.29.1-beta.0
2026-04-26 22:52:44 +02:00
github-actions[bot] 3d5759738f chore(beta): release 3.29.1-beta.0 2026-04-26 20:52:07 +00:00
Paul Nothaft 2f2f405d9b Merge pull request #319 from the-luap/feat/prezip-and-photo-replace
fix: discussion #317 issues and #318 archive crash
2026-04-26 22:51:46 +02:00
Paul Nothaft 6cfff6f6a6 fix: address bugs and feature requests from discussion #317
- Share link: display and copy now use the absolute URL built from the
  current origin instead of the relative path stored in events.share_link.
  Added a Copy Link button to the events list (inline + dropdown).
- Detect dev tools default: event creation now reads the global
  enable_devtools_protection app setting instead of always falling back to
  the column default; admins who disable it globally get new events with
  it disabled too.
- Require password default: added a global "Require password by default"
  setting (event_default_require_password, default true), exposed via
  Settings -> Events. Create-event form initialises from it.
- Filter bar: added gallery_show_filter_bar setting and hide the search/
  sort row in the public gallery when off, or when the gallery has zero
  photos (fixes the empty-state UX from the screenshot).
- Theme picker unclickable on Create Event: memoised availableEventTypes
  so its identity is stable. The "auto-apply event-type recommended
  preset" effect was firing on every render due to the unstable array
  reference and silently overwriting the user's preset selection ~1ms
  after each click.
- Branding logo disappearing on theme change: handlePresetChange and
  handleThemeChange no longer wipe the existing logoUrl when a preset
  config (which carries no logoUrl) is applied; handleSave falls back to
  brandingSettings.logo_url. themeMutation now invalidates the
  admin-settings and public-settings caches so saved theme changes appear
  immediately.
2026-04-26 22:48:59 +02:00
Paul Nothaft e4b0f961b7 fix: prevent backend crash on archive when admin_email is null (#318)
Archiving an event with no admin_email queued an email_queue row with
recipient_email=null, violating the NOT NULL constraint. The error was
thrown inside the output.on('close') callback (detached from the caller),
becoming an unhandled rejection that crashed Node and dropped admin
sessions on bulk archive.

- Skip queueEmail when event.admin_email is null/empty (admin_email has
  been nullable since migration 073).
- Wrap the close handler in try/catch so any post-archive failure logs
  instead of crashing the process.
2026-04-26 22:15:00 +02:00
Paul Nothaft c0989796e4 Merge pull request #315 from the-luap/release-please--branches--beta
Build and Push Docker Images / build-backend (push) Failing after 4m3s
Build and Push Docker Images / build-frontend (push) Failing after 4m3s
Build and Push Docker Images / summary (push) Successful in 3s
chore(beta): release 3.29.0-beta.0
v3.29.0-beta.0
2026-04-23 16:59:00 +02:00
github-actions[bot] 82adcd1f71 chore(beta): release 3.29.0-beta.0 2026-04-23 14:54:21 +00:00
Paul Nothaft d3f1206816 Merge pull request #314 from the-luap/feat/prezip-and-photo-replace
feat: pre-zip download all and photo replacement by name (#312, #313)
2026-04-23 16:49:59 +02:00
Paul Nothaft e18afd3e6b feat: pre-zip download all and photo replacement by name (#312, #313)
Pre-zip downloads:
- Generate ZIP in background after photo mutations (upload/delete/watermark change)
- Serve cached zip with Content-Length for instant downloads and native progress bar
- Falls back to on-the-fly streaming when no cache exists yet
- Frontend uses browser-native download when zip is ready (no blob buffering)
- New downloadZipService with debounced regeneration and in-memory locking

Photo replacement:
- Admin upload form gets "Replace existing photos with same name" checkbox
- Matches by original_filename (case-insensitive) within the same event
- Preserves photo ID, position, feedback, category, and visibility
- Updates file, thumbnail, dimensions, EXIF capture date on replacement
- Ambiguous matches (multiple photos with same name) skip replacement with warning
- New photoReplacementService with findReplacementCandidate and replacePhoto
2026-04-23 16:49:31 +02:00
Paul Nothaft 4353acebf9 Merge pull request #311 from the-luap/release-please--branches--beta
Build and Push Docker Images / build-frontend (push) Failing after 3m41s
Build and Push Docker Images / build-backend (push) Failing after 3m42s
Build and Push Docker Images / summary (push) Successful in 2s
chore(beta): release 3.28.3-beta.0
v3.28.3-beta.0
2026-04-13 13:01:08 +02:00
github-actions[bot] 89f86b9fe4 chore(beta): release 3.28.3-beta.0 2026-04-13 05:30:51 +00:00
Paul Nothaft ceb2a09f48 Merge pull request #310 from the-luap/fix/welcome-message-and-guest-thumbnails
fix: revert /api prefix in adminPhotos.js to avoid double-prefix (#307)
2026-04-13 07:30:28 +02:00
Paul Nothaft 094276d3cc fix: revert /api prefix in adminPhotos.js to avoid double-prefix
AdminPhotoGrid uses AdminAuthenticatedImage which fetches via Axios
(baseURL: /api), so the backend URL must not include /api — Axios
adds it. The adminGuests.js /api prefix is correct because its
consumer (AuthenticatedImage) uses fetch() with buildResourceUrl().
2026-04-13 07:30:08 +02:00
Paul Nothaft 59b56ed3d7 Merge pull request #309 from the-luap/release-please--branches--beta
Build and Push Docker Images / build-backend (push) Failing after 3m33s
Build and Push Docker Images / build-frontend (push) Failing after 3m33s
Build and Push Docker Images / summary (push) Successful in 3s
chore(beta): release 3.28.2-beta.0
v3.28.2-beta.0
2026-04-12 21:01:11 +02:00
github-actions[bot] 0a5b07de5d chore(beta): release 3.28.2-beta.0 2026-04-12 18:58:40 +00:00
Paul Nothaft b05c36ac81 Merge pull request #308 from the-luap/fix/welcome-message-and-guest-thumbnails
fix: display welcome message in gallery and fix guest thumbnail URLs (#306, #307)
2026-04-12 20:58:24 +02:00
Paul Nothaft 9323befdd9 fix: display welcome message in gallery and fix guest thumbnail URLs (#306, #307)
- Render welcome_message in gallery view for all non-fullpage layouts
  (grid, masonry, carousel, timeline, mosaic) as a centered banner
- Add /api prefix to thumbnail/photo URLs in adminGuests.js and
  adminPhotos.js so they route correctly through Nginx proxy
2026-04-12 20:58:02 +02:00
Paul Nothaft 61142c0d0e Merge pull request #305 from the-luap/release-please--branches--beta
Build and Push Docker Images / build-frontend (push) Failing after 3m40s
Build and Push Docker Images / build-backend (push) Failing after 3m41s
Build and Push Docker Images / summary (push) Successful in 3s
chore(beta): release 3.28.1-beta.0
v3.28.1-beta.0
2026-04-12 10:06:43 +02:00
github-actions[bot] 623ab72916 chore(beta): release 3.28.1-beta.0 2026-04-12 08:06:08 +00:00
Paul Nothaft 3716ff5085 Merge pull request #304 from the-luap/fix/gallery-sort-direction-and-feedback-visibility
fix: apply sort direction in gallery and respect show_feedback_to_guests (#302, #303)
2026-04-12 10:05:52 +02:00
Paul Nothaft dffe057772 fix: apply sort direction in gallery view and respect show_feedback_to_guests (#302, #303)
- Gallery now respects the configured sort direction (asc/desc) from
  default_photo_sort setting instead of using hard-coded directions
- Photos endpoint zeroes out feedback fields (like_count, favorite_count,
  average_rating, comment_count, has_feedback) when show_feedback_to_guests
  is disabled, while still showing data to admin/client users
2026-04-12 10:05:24 +02:00
Paul Nothaft 3319a304ce Merge pull request #301 from the-luap/release-please--branches--beta
Build and Push Docker Images / build-frontend (push) Failing after 3m48s
Build and Push Docker Images / build-backend (push) Failing after 3m49s
Build and Push Docker Images / summary (push) Successful in 3s
chore(beta): release 3.28.0-beta.0
v3.28.0-beta.0
2026-04-11 23:24:59 +02:00
github-actions[bot] c303dd51e8 chore(beta): release 3.28.0-beta.0 2026-04-11 21:24:41 +00:00
Paul Nothaft b1dfbe4c2f Merge pull request #300 from the-luap/feat/cookie-secure-auto
feat: add COOKIE_SECURE=auto for mixed HTTPS/HTTP deployments (#298)
2026-04-11 23:24:24 +02:00
Paul Nothaft 54badefc51 Merge pull request #299 from the-luap/fix/guest-masonry-lightbox
fix: guest feedback flow bugs in Masonry grid and PhotoLightbox (#292)
2026-04-11 23:24:02 +02:00
Paul Nothaft 15a8ab41fd feat: add COOKIE_SECURE=auto for mixed HTTPS/HTTP deployments (#298)
Adds a third value for the COOKIE_SECURE environment variable that
decides the cookie Secure flag per-request based on req.secure. This
unblocks a common self-hosted setup where the same PicPeak deployment
is reachable over both HTTPS (via reverse proxy) and plain HTTP (e.g.
LAN access at http://192.168.x.x:3001).

Behavior

  unset  - legacy default: follows NODE_ENV (production=true, dev=false)
  true   - always set Secure (unchanged)
  false  - never set Secure (unchanged)
  auto   - NEW: use req.secure per request. In practice this means
           Secure on HTTPS requests (when X-Forwarded-Proto: https
           reaches Express via a trusted proxy) and no Secure flag
           on plain HTTP requests.

The existing trust proxy config (`app.set('trust proxy',
'loopback, linklocal, uniquelocal')` in server.js) means
X-Forwarded-Proto is honored when forwarded from local/private-network
proxies, which covers Docker network setups and most self-hosted
deployments behind NPM, Traefik, or Caddy.

auto is strictly opt-in. The default behavior is unchanged, so existing
users see no difference. A follow-up release can consider promoting
auto to the default after real-world feedback.

Also fixed (latent bug, benefits everyone)

Cookie clear operations (clearAdminAuthCookie, clearGalleryAuthCookies)
previously wrote the same `secure` attribute as the set path. When a
cookie was set with Secure=true over HTTPS and the clear request came
over HTTP (or vice versa under auto mode), some browsers would reject
the Set-Cookie delete header, leaving the cookie in place. Browsers
match cookies by (name, domain, path) for deletion and don't care about
Secure, so the new buildClearCookieOptions() helper simply omits the
secure attribute.

Implementation

- secureCookie string is replaced by secureCookieMode which can hold
  true, false, or 'auto'.
- New resolveSecureFlag(res) returns the boolean for a specific
  response, delegating to res.req.secure when in auto mode.
- buildCookieBaseOptions and buildCookieOptionsWithExpiry now take res
  and pass it through.
- New buildClearCookieOptions() deliberately omits `secure`.
- setAdminAuthCookie / setGalleryAuthCookies / clearAdminAuthCookie /
  clearGalleryAuthCookies all updated to thread res where needed.
  Public signatures unchanged — every caller already has res in scope.

Testing

Verified against a real Express instance inside the backend container
with trust proxy configured, covering:

  - (unset) + NODE_ENV=production -> secure: true (legacy)
  - (unset) + NODE_ENV=development -> secure: false (legacy)
  - COOKIE_SECURE=true + req.secure=false -> secure: true (literal wins)
  - COOKIE_SECURE=false + req.secure=true -> secure: false (literal wins)
  - COOKIE_SECURE=auto + X-Forwarded-Proto: https -> secure: true
  - COOKIE_SECURE=auto + plain HTTP -> secure: false
  - clearCookie always omits the secure attribute

Documentation

Added a COOKIE_SECURE block to both .env.example files (root for
docker-compose, backend/.env.example for native install) explaining the
four values, when to use auto, and the two requirements (proxy must
forward X-Forwarded-Proto, proxy IP must be in the trust list). Also
documented COOKIE_SAMESITE and COOKIE_DOMAIN alongside, which were
previously undocumented.
2026-04-11 22:41:15 +02:00
Paul Nothaft 77f07e9329 fix: guest feedback flow bugs in Masonry grid and PhotoLightbox (#292)
Fixes two bugs reported on #292 after 3.27.0-beta.0 shipped:

1. Masonry grid showed no visual feedback after liking a photo.
   MasonryGalleryLayout's Like button had no liked-state plumbing —
   the Heart icon was a static <Heart> regardless of whether the user
   had liked the photo.

2. PhotoLightbox (fullscreen view) silently failed to like photos in
   guest identity mode. submitLike() and submitRating() never called
   ensureIdentity() before firing the API request, so the first
   interaction from a fresh session hit a 401 from the server instead
   of opening the name prompt.

Root causes:

1. MasonryGalleryLayout was missing the 'liked' state pattern that
   GridGalleryLayout already uses (likedPhotoIds Set in the parent,
   passed down as a `liked` prop, updated via onLikeSuccess callback).
   The bug was invisible in simple mode (no personal state) but
   surfaced immediately in guest mode where each guest expects to see
   confirmation of their own action.

2. PhotoLightbox's submit handlers were written before the guest
   identity context existed and only checked the legacy
   require_name_email flag. They were never updated when guest mode
   landed.

Also fixed: z-index conflict where the GuestNamePromptModal (z-50)
was sitting at the same level as PhotoLightbox (z-50), so when the
prompt opened over the lightbox, the fullscreen image intercepted
pointer events and the modal's Continue button was unclickable.
Bumped both guest modals to z-[60].

Changes:

- MasonryGalleryLayout.tsx
  - MasonryPhotoProps gains `liked?: boolean` + `onLikeSuccess?: () => void`.
  - Like button: red bg + filled white Heart icon when liked; aria-label
    toggles between "Like photo"/"Unlike photo"; aria-pressed mirrors state.
  - onClick wires onLikeSuccess() for optimistic UI in both guest-mode
    and simple-mode branches plus the FeedbackIdentityModal onSubmit path.
  - Parent layout holds `likedPhotoIds: Set<number>` and passes it to
    each MasonryPhoto (matches the GridGalleryLayout pattern).

- PhotoLightbox.tsx
  - Consumes useGuestIdentityOptional(); new `isGuestMode` flag.
  - submitLike() and submitRating() get a guest-mode branch that calls
    ensureIdentity() first and submits without body guest_name/email
    (server reads from the verified token).
  - Optimistic UI updates happen after successful submit in guest mode.

- GuestNamePromptModal.tsx, GuestRecoveryModal.tsx
  - z-50 → z-[60] so they render above PhotoLightbox.

Verified end-to-end against local Docker with Playwright MCP on event
168 (Masonry Columns Test layout):

- Fresh session, click Like in Masonry grid → name prompt opens, register,
  feedback persists with guest_id, Heart button turns red with
  aria-pressed and "Unlike photo" label. Subsequent likes on other
  photos also show red state. DB confirms feedback rows.

- Fresh session, open photo in lightbox BEFORE registering → click Like,
  the name prompt correctly opens on top of the lightbox, register,
  feedback persists. Rate 4 stars → works, average 4.0 (1) displayed
  in lightbox, ★ badge appears on toggle-feedback button, grid cell
  shows "1 likes" + "Rating: 4.0" indicators after closing lightbox.

- Backend DB: gallery_guests row created, photo_feedback rows have
  correct guest_id, server reads name from verified token (body values
  ignored).

Out of scope (documented in audit, not reported by the user, no
regression from guest mode): Mosaic/Carousel/Timeline have partial
optimistic-UI issues unrelated to this report; they pre-date guest
mode and behave the same in simple mode. Leaving alone per scope
discipline.
2026-04-11 14:42:28 +02:00
Paul Nothaft 72c0c2d18e Merge pull request #297 from the-luap/release-please--branches--beta
Build and Push Docker Images / build-frontend (push) Failing after 3m42s
Build and Push Docker Images / build-backend (push) Failing after 3m42s
Build and Push Docker Images / summary (push) Successful in 2s
chore(beta): release 3.27.0-beta.0
v3.27.0-beta.0
2026-04-11 09:27:58 +02:00
github-actions[bot] 95d8bc4065 chore(beta): release 3.27.0-beta.0 2026-04-11 06:29:51 +00:00
Paul Nothaft 3856ba25bb Merge pull request #295 from the-luap/feat/guest-selections
feat: guest selections with per-person identity (#292)
2026-04-11 08:27:31 +02:00
Paul Nothaft 9e1ba4f851 Merge pull request #296 from the-luap/release-please--branches--beta
chore(beta): release 3.26.2-beta.0
2026-04-11 08:27:08 +02:00
github-actions[bot] b0efd32f7a chore(beta): release 3.26.2-beta.0 2026-04-11 06:26:39 +00:00
Paul Nothaft 9ed8a2b199 Merge pull request #294 from the-luap/fix/admin-photo-feedback-filters
fix: admin photo feedback filters have no effect (#293)
2026-04-11 08:26:19 +02:00
Paul Nothaft ad4e5a7506 feat: guest selections with per-person identity (#292)
Introduces a new "Per-guest selections" identity mode for event
feedback, letting each visitor register under their own name so their
likes/favorites/comments/ratings are tracked independently. Includes
admin insights (list, per-guest detail, aggregate view, export) and
advanced identity features (forget-me, email recovery, invite tokens,
merge).

New event-level setting
- event_feedback_settings.identity_mode = 'simple' | 'guest' (default
  'simple' → zero behavior change for existing events).
- Admin UI radio under Feedback Settings to toggle per event.

Root cause of the previous "all guests share state" bug
- generateGuestIdentifier() was sha256(ip + userAgent), so every visitor
  on the same WiFi + similar device collided into one identity.
- Now: when a verified guest JWT is present (x-guest-token header),
  req.guest.identifier takes precedence — per-person rate limits and
  per-person deduplication.

Phase 1 — identity layer
- Migration 078: new gallery_guests, guest_invites, guest_verification_
  codes tables; identity_mode column + check constraint; nullable
  guest_id FK on photo_feedback.
- New guest JWT type scoped to (eventId, guestId).
- New middleware guestAuth.resolveGuest (non-blocking) + requireGuest.
- POST /gallery/:slug/guest, GET /guest/me, DELETE /guest/me.
- Gallery feedback route enforces guest identity in guest mode and
  reads name/email from the verified token (never from the body).
- Frontend GuestIdentityContext + GuestNamePromptModal; axios
  interceptor injects x-guest-token on gallery API calls.
- Feedback-only blocking: gallery opens freely, prompt only on first
  interactive feedback action.
- Admin "Guests" tab (conditional on identity_mode='guest') with the
  AdminGuestsList component.

Phase 2 — admin insights
- GET /admin/events/:eventId/guests list + aggregated counts.
- GET /admin/events/:eventId/guests/:guestId detail with per-type
  groupings; AdminGuestDetail modal with thumbnail grid + tabs.
- GET /admin/events/:eventId/guests/aggregate sorted by distinct guest
  pick count; GuestSelectionsAggregate component.
- Per-guest export (txt/csv/json) and bulk export-all ZIP.

Phase 3 — polish
- 3.1 Self-service forget-me link in gallery footer.
- 3.2 Email-based identity recovery: POST /guest/recover sends a
  6-digit code via the existing emailProcessor, POST /guest/verify
  exchanges it for a token (rate-limited, enumeration-safe).
- 3.3 Admin invite tokens: pre-mint identities, share URLs with
  ?invite=, single-use redemption stripping the param from history.
- 3.4 Admin merge endpoint reassigns feedback + soft-deletes sources.

Shared helper
- useGalleryFeedbackAction hook wraps the identity-check logic for
  inline like buttons across Masonry/Grid/Justified/Mosaic/Carousel/
  Timeline/Premium layouts.

Backwards compatibility
- Existing events default to 'simple' after migration; behavior
  unchanged.
- Legacy photo_feedback rows keep guest_id NULL; admin shows them in
  the generic feedback moderation view as before.
- feedback_count denormalized stat now uses COALESCE(guest_id,
  guest_identifier) so per-guest counts are accurate without touching
  legacy rows.

Verified end-to-end against local Docker
- Migration clean on existing data.
- Simple mode unchanged (no prompt, legacy flow).
- Guest mode: Alice registers on click, tokens persist in
  sessionStorage, feedback rows carry guest_id.
- Carol via invite link auto-redeems, sees Alice's "1 likes" badge.
- Admin Guests tab shows both with correct counts; detail modal
  displays thumbnail grid with badges; aggregate view sorts by picker
  count (photo 227 = 2, others = 1); CSV/JSON export matches DB.
- Merge Carol into Alice: feedback reassigned, Carol soft-deleted,
  Alice count = 4.
2026-04-11 07:48:23 +02:00
Paul Nothaft d4b4dc628f fix: wire admin photo feedback filters into grid query (#293)
The Has Likes / Has Favorites / Has Comments checkboxes in the admin
Event > Photos tab updated local state but never affected the visible
photo grid, because the feedbackFilters state was only wired to the
export menu and the backend /admin/photos/:eventId/photos endpoint had
no support for these params.

Fixes:
- backend/src/routes/adminPhotos.js: extend GET /:eventId/photos to
  accept has_likes, has_favorites, has_comments, min_rating, and logic
  (AND/OR) query params and apply them via where-clause groups using
  the existing denormalized like_count/favorite_count/comment_count/
  average_rating columns.
- frontend/src/services/photos.service.ts: add hasLikes, hasFavorites,
  hasComments, minRating, logic to the PhotoFilters interface and
  append them as query params in getEventPhotos.
- frontend/src/pages/admin/EventDetailsPage.tsx: merge feedbackFilters
  into combinedPhotoFilters (via useMemo) and key the admin-event-photos
  query on it, so toggling any checkbox refetches with the new params.

Verified end-to-end against local Docker: seeded event with a known
feedback distribution and confirmed
- Has Likes → 4 photos
- Has Favorites → 3 photos
- Likes AND Favorites → 1 photo
- Likes OR Favorites → 6 photos
- Has Comments → 2 photos
- network requests carry the exact query params
2026-04-11 07:46:32 +02:00
Paul Nothaft fe46e4268d Merge pull request #288 from the-luap/release-please--branches--beta
Build and Push Docker Images / build-frontend (push) Failing after 3m59s
Build and Push Docker Images / build-backend (push) Failing after 4m0s
Build and Push Docker Images / summary (push) Successful in 3s
chore(beta): release 3.26.1-beta.0
v3.26.1-beta.0
2026-04-09 16:40:53 +02:00
github-actions[bot] 1f3b9c6712 chore(beta): release 3.26.1-beta.0 2026-04-09 14:27:12 +00:00
Paul Nothaft 5295516b67 Merge pull request #290 from the-luap/docs/fix-filesystem-gallery-docs
docs: clarify file system photo import requires existing event (#269)
2026-04-09 16:26:51 +02:00
Paul Nothaft ee0baafc59 docs: clarify file system photo import requires existing event (#269)
The "Method 2: File System" section in SIMPLE_SETUP.md implied you
could create a gallery by just copying files to the storage directory.
In reality, the event must exist in the database first — the file
watcher only adds photos to existing events.

Rewritten to clarify the prerequisite and explain how the file watcher
works (2s stability delay, supported formats, auto-thumbnailing).
2026-04-09 16:26:39 +02:00
Paul Nothaft c63bc47089 Merge pull request #289 from the-luap/fix/password-change-regular-modal
fix: apply password change fix to regular modal + longer toast delay (#263)
2026-04-09 16:06:13 +02:00