Commit Graph

525 Commits

Author SHA1 Message Date
github-actions[bot] 2e828fcf9f chore(beta): release 3.51.1-beta.0 2026-05-16 22:53:14 +00: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
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 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
github-actions[bot] d4198e1bbc chore(beta): release 3.49.6-beta.0 2026-05-14 19:37:31 +00:00
Paul Nothaft 87834a7fff fix(install): defer events.hero_photo_id FK to break circular reference (#484)
Real root cause behind MrGabri's fresh-Postgres install crash, surfaced
by his second log dump after #488 silenced the FATAL noise:

  Initial setup failed: error: alter table "events" add constraint
  "events_hero_photo_id_foreign" foreign key ("hero_photo_id")
  references "photos" ("id") on delete SET NULL
  - relation "photos" does not exist

initializeDatabase() in src/database/db.js declared the FK inline at
events createTable (line 89), but the photos table is created later
in the same function (line 203). On Postgres this is a hard error —
the referenced table must exist at FK-declaration time. SQLite
silently tolerated it because its FK enforcement is lazy and the
inline declaration just became a column with no FK metadata.

Why no existing Postgres install hit it: initializeDatabase only
runs the createTable block on `if (!hasEventsTable)`. Once a
deployment has the events table from any prior run, the path is
skipped. So the bug only ever fires on a truly fresh Postgres
install — which is exactly MrGabri's scenario, and which our smoke
suite never exercises (it runs against a long-lived dev stack).

Fix:

- events createTable: drop the inline FK; column declared as a plain
  integer with an explainer comment.
- After both tables exist (post photos createTable): db.schema
  .alterTable('events').foreign('hero_photo_id').references...
  Wrapped in a try/catch that swallows "already exists" so re-runs
  on installs that previously got into a half-state don't fail boot.

Verified by docker compose down -v + up against the dev stack — no
FK error, all migrations apply, FK present in pg_constraint with
the expected definition.
2026-05-14 21:21:23 +02:00
github-actions[bot] 1e4f762871 chore(beta): release 3.49.5-beta.0 2026-05-14 19:02:43 +00:00
Paul Nothaft b6b58d0659 fix(admin-users): normalise date fields to ISO across DB drivers (#485)
Admin > Users page crashed with "TypeError: e.split is not a function"
on native installs (SQLite default). Reported by @blazmaric in #485
with a clean diagnosis: SQLite returns lastLogin / createdAt /
updatedAt as integer milliseconds since epoch, while Postgres
returns ISO strings via the standard JSON serialiser. The page used
parseISO() on the raw value and parseISO trips on numbers.

Fix at both layers — defence in depth:

- backend/src/routes/adminUsers.js: new toIso() helper applied in
  transformUser + transformInvitation. Coerces Date / number /
  numeric-string / null to a single ISO 8601 string contract before
  the response leaves the API. Protects every consumer (frontend
  AND external API tokens / n8n) regardless of which DB driver is
  underneath.
- frontend/src/services/userManagement.service.ts: same helper as
  defence-in-depth for stale backends mid-deploy and any cached
  pre-fix response shape. Also surfaced an existing
  transformInvitation gap — invitations endpoints were returning
  raw response.data.invitations without going through the
  transformer.

10 unit tests pin the toIso contract: all known driver shapes
(Date, number, numeric-string, ISO-string, null/undefined/empty)
plus the full transformer paths for transformUser and
transformInvitation.

Out of scope: same epoch-ms surface may exist on other admin pages
that were never tested against SQLite (events list, customers,
webhooks, api tokens, activity log). Worth a follow-up audit pass
to apply toIso() in every snake_case→camelCase transformer the
admin routes use, but the immediate Users-page crash is the only
reported one and shipping that fix unblocks @blazmaric.
2026-05-14 20:53:10 +02:00
github-actions[bot] 57ea08e1ed chore(beta): release 3.49.4-beta.0 2026-05-14 18:49:31 +00:00
github-actions[bot] bc58520cd2 chore(beta): release 3.49.3-beta.0 2026-05-14 18:18:43 +00:00
Paul Nothaft a803491cf4 fix(promo-banner): center by default + admin alignment selector (#482)
The gallery promotional banner (#440) read as visually offset from
the gallery footer because:

  - Footer used `container text-center px-4` (full container width,
    centered text).
  - Promo block used `container py-4 sm:py-6` with an inner
    `max-w-3xl mx-auto` wrapper holding left-aligned text — a
    narrower column with left-aligned content sitting in the
    middle of the page.

Two issues compounded: the column was narrower than the footer AND
its text alignment differed. Reported by Rekoo-PS in #482 with a
screenshot showing the misalignment, with a request for an admin
alignment option.

Fix:

- Drop the inner max-w-3xl wrapper. Promo content now spans the
  same .container width as the footer, eliminating the
  narrower-column visual.
- Default text alignment changed from left → center to match the
  footer.
- New `branding_promo_alignment` setting ('left' | 'center' | 'right',
  default 'center'). Surfaced as a dropdown next to the existing
  Position dropdown on the BrandingPage. Live preview block on the
  BrandingPage mirrors the gallery render so admins see what
  guests will see.
- Also replaced the no-op `prose-sm` prose-modifier with a real
  `prose prose-sm` outer class so the existing `prose-a:text-accent`
  modifier actually takes effect (it didn't before — modifiers
  without an outer .prose are silently ignored by Tailwind
  Typography).

Migration 103 seeds the new setting at 'center' so existing
installs that have a promo banner today see the corrected
alignment immediately on next deploy.

i18n: en + de hand-translated; nl/pt/ru/fr machine-translated and
flagged for native review per project convention.
2026-05-14 20:10:14 +02:00
github-actions[bot] 1cbb0c4cff chore(beta): release 3.49.2-beta.0 2026-05-13 16:46:19 +00:00
github-actions[bot] e5ef893395 chore(beta): release 3.49.1-beta.0 2026-05-13 16:31:12 +00:00
github-actions[bot] b3c6508712 chore(beta): release 3.49.0-beta.0 2026-05-13 16:18:38 +00:00
Paul Nothaft 0bc7e2af17 feat(og): per-event opt-in to use hero photo as social-share preview (#474)
Background: galleryOgService already serves OG/Twitter Card meta tags
to social-crawler User-Agents (WhatsApp, Facebook, Slack, Telegram,
Discord, ~21 in total) for /gallery/:slug URLs. Today the og:image
is always the brand logo with the inline rationale "no protected
photo content".

#474 asked for a hero/cover photo preview. The trade-off is that any
URL embedded in og:image is fetched unauthenticated by every
link-preview crawler — so an opted-in image is effectively public
to anyone the gallery URL is shared to. Ship as a per-event boolean,
default FALSE, so existing galleries never start surfacing photos
without explicit admin intent.

Schema (migration 102):
  - events.og_image_share_enabled BOOLEAN NOT NULL DEFAULT FALSE.

Backend:
  - galleryOgService.buildOgMetadata: when opt-in is on AND a
    hero_photo_id is set AND the photo has a generated thumbnail,
    emit og:image as /og/gallery/:slug/cover. Falls back to the
    brand logo on any miss (deleted hero, missing thumbnail, no
    opt-in) so a half-configured gallery still gets a polished
    preview rather than a broken-image src.
  - galleryOgService.handleGalleryOgCover: new public endpoint that
    streams the hero thumbnail. Validates slug shape, checks the
    opt-in flag + hero presence + thumbnail existence; returns 404
    on any failure. ETag = thumbnail mtime + photo id so a
    regenerated thumb busts crawler caches. Cache-Control:
    public, max-age=300 (short — admins shouldn't wait an hour for
    a cover swap to land in chat previews).
  - server.js: mount the new GET /og/gallery/:slug/cover route. The
    existing nginx ^~ /og/gallery/ proxy block already covers it.
  - adminEvents.js: validator + persistence on POST + PUT.
    formatBoolean coercion so SQLite (0/1) and Postgres (boolean)
    both behave correctly.

Frontend:
  - Event type + UpdateEventData carry og_image_share_enabled.
  - EventDetailsPage adds a checkbox under the HeroPhotoSelector,
    disabled when no hero photo is picked. Help text deliberately
    spells out the public-by-design consequence — admins shouldn't
    flip this on for a sensitive gallery without realising what
    they're sharing with link-preview crawlers.

Tests: 8 new in galleryOgService.shareImage.test.js — pin the
cover-vs-logo decision contract (3 cases) plus the defensive
fallbacks (deleted hero, missing thumbnail) and the 404 contract
on the cover endpoint (4 cases). The 404 tests assert that
ensureThumbnail() is NOT called when opt-in is off, so a future
refactor can't accidentally widen the unauthenticated cover
endpoint to expose a hero the admin hasn't shared.

i18n: en + de hand-translated; nl + pt + ru + fr machine-translated
and flagged for native review per project convention.
2026-05-13 13:47:02 +02:00
github-actions[bot] fa71e7ea1e chore(beta): release 3.48.1-beta.0 2026-05-12 20:57:36 +00:00
Paul Nothaft 3122dd08a8 fix(customer-routes): Cache-Control: no-store on customer endpoints (#470)
The trigger: PR #458 mounted requireCustomerPortalEnabled which
410'd every /api/customer/* + /api/admin/customers/* request when
the master toggle was off. Some browsers cached that 410 (no
Cache-Control header was set, so heuristic freshness applied —
the wrong default for an authenticated/sensitive surface).
PR #470 reverted the middleware, but a customer whose tab cached
the 410 still saw 410s until they hard-refreshed.

Add noStoreCache middleware and mount it in front of both route
groups. Every response (200, 4xx, 5xx) now carries
`Cache-Control: no-store, no-cache, must-revalidate, private`
plus the HTTP/1.0 Pragma + Expires fallbacks. Any future
transient error from these endpoints can no longer get pinned in
browser or proxy caches and outlive its cause.

Cost is one setHeader per request; applied per route group rather
than globally so static assets + galleries keep their own caching
strategy unchanged.

Includes a dedicated unit test pinning the header set so a future
cleanup pass can't quietly drop it and re-introduce the bug.
2026-05-12 22:53:49 +02:00
Paul Nothaft 5e86eef4f8 test(gallery): verifyGalleryAccess customer-assignment revocation (#470)
4 unit tests pinning the contract of the customer-minted JWT
re-check added in #470:

- via='customer' + customerId, assignment present → next() runs.
- via='customer' + customerId, assignment removed → 403 with
  CUSTOMER_ASSIGNMENT_REVOKED code.
- customerId in payload but `via` claim missing → no re-check
  (defends against a future refactor accidentally widening the
  gate to match every legacy session that happens to carry a
  customerId field).
- per-event-password JWT (no via, no customerId) → no
  event_customer_assignments query at all (asserted by counting
  db() invocations — a regression that quietly added a re-check
  here would 403 every guest the moment any unrelated customer
  was unassigned from any event).

Same mock pattern as customerAuth.middleware.test.js. The re-check
is the load-bearing piece behind the "Manage galleries" dialog
UX promise — these tests guard it explicitly.
2026-05-12 22:53:23 +02:00
Paul Nothaft 7a9c4ca44e test(customers): unit-cover setAssignmentsForCustomer (#470 follow-up)
5 new tests covering the diff math (added/removed), the
archived-event filter, the no-op short-circuit when wanted equals
existing, and the type-coercion of the wanted-list input. Mirrors
the existing setAssignmentsForEvent suite shape so the inverse-
direction service function carries equivalent regression coverage.

This function is the writer behind the "Manage galleries" dialog
and the verifyGalleryAccess re-check together form the access-
control story for the whole feature — getting the diff math
wrong here means assignments don't actually revoke, which is the
entire promise of the new UI.
2026-05-12 22:53:05 +02:00
github-actions[bot] dafc8d041a chore(beta): release 3.48.0-beta.0 2026-05-12 20:48:26 +00:00
Luca c02c947463 feat(customers): email customer when admin adds new gallery access 2026-05-12 01:25:12 +02:00
Luca 3f4419356a revert(customer-portal): make the global flag UI-only, drop the kill-switch middleware 2026-05-12 00:40:17 +02:00
Luca 55a5846f6f feat(gallery): revoke customer-minted JWTs when assignment is removed 2026-05-11 23:39:47 +02:00
Luca 5377b88e0e feat(customers): replace-assignments endpoint for a single customer 2026-05-11 23:39:22 +02:00
github-actions[bot] a5e58d085e chore(beta): release 3.47.2-beta.0 2026-05-11 20:15:57 +00:00
github-actions[bot] d9d52ec8ab chore(beta): release 3.47.1-beta.0 2026-05-11 19:57:31 +00:00
github-actions[bot] 1c23ec51ec chore(beta): release 3.47.0-beta.0 2026-05-11 19:44:45 +00:00
Paul Nothaft 4f3db923a6 Merge pull request #464 from Luca-Timo/feat/email-templates-reorg
Feat/email templates reorg
2026-05-11 21:44:17 +02:00
Luca 2343a162df fix(email-templates): backfill subcategory + customer password reset translations 2026-05-11 21:08:16 +02:00
Luca e3150e4213 feat(email-templates): seed missing locale translations + post-075 templates 2026-05-11 20:56:58 +02:00
Luca 2cae3fe47d feat(email-templates): categorise + sub-categorise + link to feature flags 2026-05-11 20:56:28 +02:00
Luca 358f7ee99e feat(email-templates): seed missing nl/pt/ru/fr translations 2026-05-11 20:42:28 +02:00
Luca 84c06affb7 feat(email-templates): categorise + link to feature flags 2026-05-11 20:41:50 +02:00
github-actions[bot] b31fc2140b chore(beta): release 3.46.3-beta.0 2026-05-11 18:18:34 +00:00
github-actions[bot] eddcdbf4d8 chore(beta): release 3.46.2-beta.0 2026-05-11 18:07:41 +00:00
Paul Nothaft 9776d8a6fc Merge pull request #458 from Luca-Timo/fix/customer-functions
fix(customer-portal): post-merge fixes for event save, theme fonts, and customer→gallery handoff
2026-05-11 20:07:11 +02:00
Luca c0c6b4c0e8 chore(customer-portal): align flag-gate comments with new dual-enforcement 2026-05-11 19:38:18 +02:00
Luca 9091ed4012 feat(clients): scaffold top-level Clients section with sub-nav around Accounts 2026-05-11 16:17:14 +02:00
Luca 75e41eba03 feat(branding): toggle login-page logo frame + size 2026-05-11 11:02:33 +02:00
Luca dde72a1b1b fix(events): strip customer_account_ids from update spread 2026-05-11 10:53:12 +02:00
github-actions[bot] 3ea7eea4b7 chore(beta): release 3.46.1-beta.0 2026-05-11 08:07:45 +00:00
Paul Nothaft 5b148542e6 Merge pull request #455 from the-luap/fix/photo-dimensions-and-default-fit
fix(import): capture photo dimensions in fileWatcher + s3AutoImporter (#447)
2026-05-11 10:07:07 +02:00
Paul Nothaft 49b36a0352 chore(migrations): renumber 090 → 096 + small notes from #403 review
Post-merge cleanups after #403 (customer portal):

- Renumber 090_backfill_photo_dimensions_v2.js → 096 to follow #403's
  090_add_customer_accounts ... 095_add_customer_portal_flag chain.
- customerAccountsService.js: TODO note on must_change_password
  documenting that the column is decorative until an admin
  pre-loaded-password flow ships (mirrors what adminAuth does for
  must_change_password today).
- customerAuth.js: doc-comment on the /login route explaining why the
  customerPortal feature flag deliberately doesn't gate it (toggle off
  hides UI, doesn't revoke existing-customer access; deactivate
  individual accounts to lock out).
- 095_add_customer_portal_flag.js: header comment said "Migration 094"
  (copy-paste from 094) — now matches the filename.
2026-05-11 09:59:47 +02:00
Paul Nothaft 936a277eb8 fix(import): capture photo dimensions in fileWatcher + s3AutoImporter (#447)
The aspect-aware gallery layouts (masonry / mosaic / justified) read
photo.width and photo.height to size each card to the source's real
proportions. Two import paths were inserting rows without those
fields, which forced MasonryGalleryLayout to fall back to a hard-coded
800×600 default — every card came out the same shape, so users
reported masonry as "always cropped to 1:1ish" no matter which
thumbnail fit mode they chose.

- fileWatcher.js: extract dims with sharp.metadata() before insert.
- s3AutoImporter.js: same, materialising a tmp local copy via
  withLocalCopy so it works in S3 mode.
- migration 090: backfill any pre-existing rows with NULL dims
  (skips videos, skips S3 deployments — those need the writer fix
  alone since migrations cannot reach the storage backend).
- imageProcessor.js: change DEFAULT_THUMBNAIL_FIT from 'cover' to
  'inside' (only kicks in when the seed setting is missing — existing
  installs keep their saved value). Add UI tooltip recommending
  'inside' for masonry/mosaic/justified, 'cover' for uniform grids.

i18n covers all six locales.
2026-05-11 09:58:08 +02:00
github-actions[bot] 87dfae0074 chore(beta): release 3.46.0-beta.0 2026-05-11 07:57:50 +00:00
Luca b252cb67eb feat(branding): Customer dashboard header toggles in Branding page
Adds back the "Show logo" / "Show company name" toggles for the
customer dashboard, scoped to /customer/* surfaces only. Lives as a
dedicated card at the bottom of Settings → Branding, gated by the
customerPortal feature flag so admins who haven't enabled the portal
don't see it.

* Backend: restored GET/PUT /admin/settings/customer-surface
  endpoints, whitelisted only to the two branding keys
  (customer_show_logo, customer_show_company_name). The
  calendar/quotes/bills feature globals that used to live on this
  endpoint are now driven by the Features tab (feature_flags table).
* customerAccountsService.getCustomerSurfaceGlobals() reads from
  app_settings again so /api/customer/auth/session honours the
  toggles in its branding payload.
* New CustomerDashboardBrandingCard component with its own save
  flow — separate from the main BrandingPage payload so flipping a
  toggle doesn't replay the full branding mutation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-11 01:52:27 +02:00