- 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.
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.
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
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().
- 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
- 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
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.
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.
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
The new token issued after password change had iat (integer seconds)
that was <= password_changed_at (millisecond precision), causing the
auth middleware's "iat < passwordChangedTime" check to reject it
immediately. Set iat explicitly to 1 second after password_changed_at.
E2E tested: login → mandatory password change → dashboard loads
successfully with no redirect loop and no 401 errors.
Add per-event default photo sort setting with 6 options:
- Upload Date (Newest/Oldest First)
- Date Taken (Newest/Oldest First) — uses EXIF captured_at
- Filename (A-Z / Z-A)
Backend:
- Migration 077 adds default_photo_sort column to events table
- Event create/update handlers accept and validate the setting
- Gallery info endpoint returns default_photo_sort for frontend
Frontend:
- "Date Taken" added to gallery sort dropdown (alongside Date, Name,
Size, Rating)
- Gallery initializes with event's default sort instead of hardcoded
"date"
- "Default Photo Sort" dropdown in event create and edit forms
- Photos without EXIF dates fall back to upload date
i18n: All 5 locales (EN, DE, NL, PT, RU) updated with sort labels.
Closes#283
#263: The mandatory password change modal updated React state before
the browser stored the new JWT cookie, causing a race condition where
the auth context checked the session with the old (invalidated) token.
Replace the state update with a full page redirect to /admin/dashboard
after a brief delay, ensuring the new cookie is applied cleanly.
#269: The file watcher service imported isVideoMimeType from
fileSecurityUtils where it doesn't exist. The function is exported
from videoProcessor. Fix the import path.
Closes#269
Draft Mode:
- Events are created as drafts by default — no email sent until published
- Add "Publish & Notify Client" button with confirmation dialog
- Draft banner with yellow styling on event details page
- Draft filter tab in events list
- Gallery middleware blocks public access to draft events
- Migration 076 adds is_draft column to events table
Admin Draft Preview:
- Admins can preview draft galleries via JWT preview token (?preview=)
- "View Gallery" link on drafts auto-appends preview token
Admin & Login Page Branding:
- Admin header uses configured company logo/name from branding settings
- Login page shows configured logo instead of hardcoded PicPeak
- Respects logo_display_mode (logo_only, text_only, logo_and_text)
OG Tag Branding:
- DynamicFavicon component updates OG meta tags and page title from
branding settings
Editable Client Email:
- Customer email is now editable after event creation in edit mode
Branding Inheritance:
- New events inherit hero logo settings (visibility, size, position)
from global branding configuration
Share Link Full Domain URL:
- New getFrontendBaseUrl() utility with DB fallback to general_site_url
- Used in email processor and share link service
After changing password, the backend sets password_changed_at which
invalidates the old JWT token. But the frontend still holds the old
token in the HttpOnly cookie, so the next session check returns 401,
triggering an infinite redirect loop between /admin/login and
/admin/dashboard.
Fix: issue a new JWT token cookie after successful password change
so the session remains valid without requiring re-login.
Replace column-based email template languages (subject_en/subject_de) with
a normalized email_template_translations table where each language is a row.
This allows adding new languages without schema changes.
- Add migration 075 to create email_template_translations table, migrate
existing EN/DE data, and seed NL/PT/RU for customer-facing templates
- Update processTemplate() to query translations table with fallback chain
(requested lang -> en -> first available), with legacy column fallback
- Restructure admin email API to return/accept translations object format
- Update frontend EmailConfigPage with dynamic 5-language tabs, translation
count badges, and copy-from-language feature for empty translations
- Add Dutch to default language dropdown in general settings
- Add Dutch to clientAccessI18n and password security messages in emails
- Expand email domain detection for NL/BE/BR/PT/RU domains
- Add email i18n keys (copyFrom, noTranslation, etc.) across all 5 locales
Use wrapEmailHtml() for the test email so it matches the look of all
other emails sent by the platform (logo, footer, etc.).
Co-authored-by: Paul Nothaft <[email protected]>
- Add Russian (Русский) to admin settings language dropdown
- Fix Premium layout hero using thumbnail instead of hero_url
- Hide "Uncategorized" section header in Story layout for uncategorized photos
- Add PhotoLightbox to Story layout so photo clicks open full-screen view
- Use full-res images in StoryPhotoCard instead of thumbnails
- Defer public gallery auto-login text until settings/locale are loaded
- Add validation and debug logging for email logo URL construction
Co-authored-by: Paul Nothaft <[email protected]>
- Set password_changed_at when changing password via adminAuth route so
existing JWT tokens are rejected by the auth middleware check
- Enforce session timeout on first request with unseen tokens by checking
token iat against configured timeout (prevents bypass after server restart)
- Convert camelCase roleId/isActive to snake_case role_id/is_active in
frontend updateUser service (fixes silent role update failures)
Resolves GHSA-rqg3-47p5-vgwg
- Fix dimension repair for external media by using photoResolver instead of hardcoded paths
- Extract photo dimensions via Sharp during external media import
- Remove duplicate theme useEffect from GalleryView to prevent flash/revert race condition
- Pass event welcome_message to Story layout footer for per-event customization
- Add email_primary_color/email_secondary_color settings with admin UI color pickers
- Add i18n keys for email branding in all 4 locales (en, de, ru, pt)
- Add photo_cap column to events table (migration 074) to limit photos per event
- Enforce photo cap in upload route, returning 400 when limit exceeded
- Pass photo_cap through all event CRUD routes and frontend forms
- Add complete Portuguese (pt-BR) translation (2300+ strings)
- Register pt locale in i18n config, language selector, date formatting
- Add photoCap/photoCapHelp translation keys to all locale files (en, de, ru, pt)
The email template preview modal was showing only raw body HTML without
the styled wrapper (green header bar, logo, footer with company name)
that processTemplate() applies when sending. This made preview not match
what recipients actually receive.
Extract wrapEmailHtml() from processTemplate() and reuse it in the
preview endpoint. Also fix logo URL to use FRONTEND_URL consistently.
Closes#229
- Fix admin video upload missing media_type/mime_type and video processing (#203)
- Fix Gallery-Premium Select All using atomic callbacks instead of stale closure loop (#220)
- Add photo dimension repair endpoint and admin UI (#180)
- Add E2E tests for all three fixes
When admin/customer emails were configured as optional in Settings >
Event Creation, the backend still rejected empty values because:
1. express-validator .optional() only skips undefined, not empty strings
— changed to .optional({ values: 'falsy' }) so "" is treated as
absent
2. DB columns host_email and admin_email had NOT NULL constraints
— added migration to make them nullable
3. Email queue insert crashed on null recipient_email
— skip queuing when no customer email is provided
Add a new "Thumbnails" tab in the admin settings page allowing users to
configure thumbnail dimensions, quality, format, and fit mode from the UI.
Also fix backend route column name mismatch (key/value → setting_key/setting_value)
that caused a 500 error, and add a button to regenerate all thumbnails.
The "Allowed File Types" admin setting was stored in the database but
never actually read during upload validation. Both frontend and backend
used hardcoded MIME type lists, causing video uploads (e.g. MP4) to be
rejected even when explicitly added to the setting.
Changes:
- Add getAllowedMimeTypes() to uploadSettings service that reads the
general_allowed_file_types DB setting and converts extensions to MIME types
- Backend admin upload route now resolves allowed types from settings
before multer processes files (via resolveAllowedTypes middleware)
- Backend gallery upload route uses dynamic allowed types from settings
- Expose allowed_file_types in public settings API for gallery clients
- Frontend PhotoUpload and UserPhotoUpload components now derive allowed
MIME types from settings instead of hardcoded image-only lists
- Add shared fileTypes.ts utility for extension-to-MIME conversion
Closes#203
Surface the existing original_filename from the database in the admin
photo grid hover overlay and photo viewer sidebar, so photographers can
correlate uploaded images with their Lightroom/disk originals. Only shown
when it differs from the system-generated filename. Gallery guests remain
unaffected.
- Disable production source maps and hide nginx version
- Reduce JSON body limit from 10gb to 50mb (uploads use multer, not JSON)
- Strip database info and error details from health endpoint
- Mask reCAPTCHA secret key in admin settings API responses
- Whitelist sort/order query parameters in events and photos endpoints
- Stop reflecting arbitrary origins in static file CORS headers
- Align nginx security headers with backend Helmet CSP, remove deprecated X-XSS-Protection
- Strip EXIF metadata from generated thumbnails and hero images
- Bind postgres/redis dev ports to localhost in docker-compose configs
- Add safeExec utility (spawn with shell:false) to prevent command injection
- Convert all exec/execAsync calls in backup, restore, and database backup
services to use safe spawn-based helpers
- Add Update Instructions Dialog with environment-specific commands (Docker/Git/Standalone)
- Add email notification settings for new version alerts
- Add "Sort by Capture Date" option using EXIF metadata extraction
- Fix E2E tests by loading environment variables via dotenv
- Add test-images/ and backend/*.db to .gitignore
Closes#181
- Fix masonry/mosaic layout regression where tiles displayed uniform heights
instead of respecting image aspect ratios. Changed from fixed 150-500px
height constraints to dynamic constraints based on column width.
- Add hero image optimization pipeline generating 1920x1080 images for
full-width hero sections instead of using low-quality thumbnails.
- New /hero/:photoId endpoint serves optimized hero images with watermark
support and automatic generation/caching.
- Add hero_url field to photos API response for frontend consumption.
- Migration 069 adds hero_path column to photos table.
Admin Dark Mode:
- Add AdminDarkModeContext with light/dark/system preference
- Update all admin components with Tailwind dark: classes
- Add dark mode toggle in admin header
- Persist preference in localStorage
SEO Settings:
- Add robots.txt configuration in Settings > SEO tab
- Block AI crawlers (GPTBot, ChatGPT-User, etc.) with toggle
- Custom robots.txt rules management
- Add RobotsMetaTags component for gallery pages
- Backend service for dynamic robots.txt generation
- Database migration for SEO settings storage
UI/UX Improvements:
- Consistent dark mode styling across all admin pages
- Update gallery components with themed CSS classes
- Fix input, card, and button styling for dark mode
The frontend never sent header_style/hero_divider_style as separate
fields when creating or updating events, so the database columns always
kept their default value of 'standard' — making the hero header
impossible to enable through the admin UI.
- Extract headerStyle/heroDividerStyle from theme config and include in
create and update payloads (CreateEventPage, EventDetailsPage)
- Add backend fallback to extract values from color_theme JSON when not
explicitly provided, ensuring older clients stay in sync
Wire up the hero_photo_id column on photo_categories that was added in
the migration but never connected. Backend routes now accept and persist
hero_photo_id on category create/update, a dedicated PUT /:id/hero
endpoint is added, and the gallery API returns hero_photo_id for each
category. Frontend EventCategoryManager shows a clickable thumbnail per
category that opens a photo picker modal. Includes EN/DE i18n keys.
Add missing i18n translations for hero image focal point picker in both
EN and DE locales. Fix lint errors across touched files: remove unused
imports/variables, replace raw buttons with shared Button component,
eliminate inline styles, extract duplicated backend validation, and
remove dead heroImagePosition type.
Add interactive focal point picker for hero images, allowing precise
crop positioning via click or preset buttons (top/center/bottom).
Includes backend validation, migrations, and gallery rendering support.
- Add try-catch and file existence check for photo path resolution (#161)
- Fix gallery categories to use photo_categories table instead of legacy type field (#156)
- Add byte-size-based chunking (500MB max) for uploads to prevent oversized batches (#155)
- Add separate header_style setting (hero/standard/minimal/none) that can
be combined with any layout type (grid/masonry/carousel/timeline/mosaic)
- Create HeroHeader and HeroDivider components for reusable hero section
- Add hero_divider_style setting (wave/straight/angle/curve/none)
- Add database migration for header_style and hero_divider_style columns
- Remove deprecated HeroGalleryLayout component
- Fix various TypeScript errors across the codebase:
- Add missing type properties (css_template_id, updatedAt, justified settings)
- Fix null handling for event_date and expires_at fields
- Fix translation function calls and i18n config
- Remove unused imports and variables
- Increase nginx client_max_body_size from 100MB to 1GB for video support
- Fix admin photo category filtering to properly handle numeric category IDs
from the photo_categories table, not just legacy 'individual'/'collage' types
- Add support for 'uncategorized' filter to show photos with no category
Add Google Photos-style justified row layout as a mode within masonry:
- Add masonryMode setting: 'columns' (Pinterest) or 'rows' (Google Photos)
- Create justifiedLayoutCalculator utility for row-based layouts
- Extract and store image dimensions on upload for layout calculations
- Include width/height in gallery API response
- Add row height and last row behavior controls to theme customizer
- Support responsive container width detection with ResizeObserver
Photos in rows mode maintain their aspect ratios while filling
horizontal rows at a consistent height. The number of photos per
row is automatically calculated based on target row height and
photo dimensions.
Closes#146
Add event-level custom logo upload/delete endpoints and UI, allowing
per-event logos to override the global branding logo in gallery views.
Also fixes several bugs discovered during testing:
- fix: category_id 'individual' parsed as NaN causing photo upload failures
- fix: gallery auth race condition where photos query fired before token stored
- fix: gallery-photos query not invalidated after favorite/like mutations
- fix: e2e test race conditions with View Gallery button detachment
Add configurable hero logo settings for individual events:
- Logo visibility toggle (show/hide in hero section)
- Logo size options (small, medium, large, xlarge)
- Logo position options (top, center, bottom)
Changes include:
- Database migration for hero_logo_visible, hero_logo_size, hero_logo_position fields
- Backend routes updated to handle new settings
- Frontend admin page with logo customization controls
- HeroGalleryLayout component with dynamic logo rendering
- i18n translations for EN and DE
Also updates .gitignore to exclude test files and artifacts.
Implements GitHub issue #139 - allows users to create and manage custom
event types beyond the default presets (wedding, birthday, corporate, other).
Backend:
- Add event_types table migration with default system types
- Create eventTypeService for CRUD operations with legacy fallback
- Add adminEventTypes routes with full REST API
- Update event validation to use dynamic event types
- Update slug generation to use custom slug_prefix
Frontend:
- Add EventTypesPage with full CRUD admin interface
- Add eventTypes.service.ts API client
- Update CreateEventPage to fetch types dynamically
- Add Event Types navigation in admin sidebar
- Add i18n translations (EN/DE)
Backward compatible: existing galleries continue to work, legacy types
accepted even if database is empty via fallback mechanisms.
Added optional chaining when accessing req.body.password in the
resend-email endpoint to handle cases where req.body is undefined.
This prevented the "Cannot read properties of undefined" error.
Fixes#137
Addresses GitHub issue #132 - enables filtering client feedback and
exporting filenames for use in Lightroom.
Changes:
- Add original_filename column to photos table via migration
- Store original filename during photo upload
- Fix export service column name mismatches (path, size_bytes, uploaded_at)
- Fix table name (photo_categories instead of categories)
- Fix toFixed() calls to handle string ratings from database
Export formats available:
- TXT with comma separator (for Lightroom Library Filter)
- CSV with full metadata
- JSON for automation
- XMP sidecar files (for Lightroom/Bridge/Capture One)