Commit Graph
41 Commits
Author SHA1 Message Date
Luca 1bf0b34ea5 feat(projects): gate Project Overview behind a projects feature flag + cockpit email actions
- Migration 120 seeds the projects flag (default OFF), idempotent.
- Backend feature-flags whitelist + DEFAULT_FLAGS + clients derivation.
- adminProjects routes 403 PROJECTS_DISABLED when the flag is off.
- projectService email actions (resend/cancel/retry/send-now) + routes.
- Frontend flag type, DEFAULT_FLAGS, Features tab card (en+de).
2026-06-06 12:59:57 +02:00
Luca b064aab8ef feat(crm): unlock reminderEmails feature flag in Features tab
The full reminder-email implementation (eventReminderService,
eventReminderTemplates self-heal, ReminderTemplatesPage,
EventReminderOverrideCard) shipped in the CRM bundle but the
FeaturesTab card kept lockedReason=NOT_YET_AVAILABLE — so the
working feature was invisible.

Flip the card to the same shape as customerPortal: status="beta",
real setFlag handler, no disabled/lockedReason. The sub-tab in
Settings → Reminder templates already self-mounts when the flag
is on, and the per-event override card already self-renders on
the event detail page.

Description copy + EN/DE i18n updated to describe what the feature
actually does (per-category pre-event nudge) instead of the old
"coming soon" placeholder.
2026-05-26 20:31:14 +02:00
Luca a7e16e7bf6 feat(crm): frontend code — pages + services + components
Brings in the full frontend CRM stack: admin authoring pages,
customer-portal surfaces, public response flows, typed services,
and the supporting component library. i18n locale JSON is the next
commit (kept separate so reviewers can read it as data).

Pages
  - Quotes: list / editor / detail / public accept-decline
  - Invoices: list / editor / detail / public payment-check
  - Contracts: list / editor / detail / block library / public sign
  - Calendar (FullCalendar — admin-only v1)
  - Tax report (period picker + CSV/PDF export)
  - Hours (logged time entries, per-customer)
  - Deals lineage (DocumentLineageCard surfaces)
  - CRM Development (admin dev tools, gated by crmDevelopment flag)
  - Customer-portal pages for quotes / invoices / contracts
  - Settings reorg: CRM-Settings group + dedicated tabs for Business
    Profile, CRM behaviour, Contracts block library, Reminder emails
  - BrandingPage typography (PDF font picker)
  - EventDetailsPage / CustomerDetailPage / CreateEventPage extensions
    (event-time fields, hours toggle, per-event reminder override)

Services (typed)
  - quotes.service, bills.service, contracts.service
  - customerAdmin.service, deals.service, calendar.service,
    taxReport.service, contracts-blocks.service
  - businessProfile.service (timezone, font picker, bank accounts)
  - useInstallmentDefaults hook, useLocalizedDate dateInputLang extension

Components (admin)
  - CustomerPicker (shared across quote/invoice/contract editors)
  - LineItemsTable (hierarchy + details_text, memoised pricing)
  - InstallmentsPanel (simple + advanced toggle, fixed-date vs trigger)
  - DocumentLineageCard (deal_uuid grouped view)
  - EditInstallmentPlanModal (atomic post-spawn plan reshape)
  - EventReminderOverrideCard, EmailTemplateEditor (tiptap),
    PdfFontPicker, IntegrityCheckCard
  - Feature-flag context + RequireFeature wrapper + AdminSidebar
    featureFlagsAny derivation + UI-hiding sweep

Build infra
  - vite.config: fullcalendar chunk carved off (~200 KB lazy-loaded)
  - frontend/package.json: tiptap, fullcalendar, signature_pad,
    react-international-phone, et al.
  - tailwind + prose styles updated for editor surfaces

3-way merge note: 1 conflict (CustomerDetailPage.tsx) hand-resolved
to keep upstream's SUPPORTED_LANGUAGES.map() data-driven pattern
over feat/crm's hardcoded option list; feat/crm's DecimalInput
import preserved alongside.
2026-05-26 18:19:32 +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 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 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
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 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
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
Paul Nothaft dec2f5d3d2 fix(features): customer-portal card uses 'Clients' to match sidebar wording
Settings → Features showed the customer-portal toggle as "Accounts"
("Konten" in DE, "Comptes" in FR, etc.) — the deeper sub-nav label
inside ClientsLayout — while the prominent menu-bar entry the admin
actually clicks first reads "Clients" / "Kunden". The mismatch was
confusing on first encounter ("which one do I look for?").

Align the Features tab card title and the "Sidebar:" callout with
the menu-bar wording (`navigation.clients`) across all six locales.
The sub-nav inside ClientsLayout keeps its own "Accounts" label —
that one matches the /admin/clients/accounts URL and is correct.
2026-05-11 21:52:42 +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 bf7ef14626 fix(settings): readable contrast on accent-tinted icon tiles + pills 2026-05-11 11:27:16 +02:00
Luca 15d01f3756 fix(features-tab): icon tiles + preview pills follow CI accent 2026-05-11 11:07:55 +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
LucaandClaude Opus 4.6 087ef45942 feat(customers): customer portal (#354) on top of feature-flags reorg
Implements the recurring-customer login surface from
the-luap/picpeak#354 plugged into the maintainer's
new feature-flag infrastructure (PR #443) instead of
a parallel toggle.

* New `customerPortal` feature flag (foundation flag for the
  not-yet-built calendar/quotes/bills/messaging customer
  surfaces). Defaults FALSE on fresh installs, TRUE on existing
  installs (events > 0) via migration 095 so live customer
  accounts don't disappear mid-deployment.
* Foundation schema: customer_accounts, customer_invitations,
  event_customer_assignments, customer_password_resets, plus
  RBAC permissions customers.view / .create / .delete granted
  to super_admin + admin system roles.
* Backend: /api/admin/customers (invite, list, search, assign,
  deactivate, reset password) + /api/customer/auth/* +
  /api/customer/* (login, dashboard, accept-invite, reset).
  Customer JWT bypass minted via
  /api/customer/events/:slug/access-token so existing gallery
  middleware stays untouched.
* Frontend: /customer/* route tree gated by RequireFeature flag
  customerPortal, with login / dashboard / accept-invite /
  reset pages and a customer-side sidebar layout.
  /admin/customers and /admin/customers/:id gated identically.
* Settings → Features grows a "Customers" section with a
  Customer portal card. The maintainer's Features tab stays the
  single source of truth — no parallel Advanced features tab.
* CustomerAccountPicker on event create/edit forms hides itself
  when the flag is off; backend ignores customer_account_ids in
  that case instead of erroring the whole event save.

Translations: en + de hand-translated. nl/pt/ru fall through to
en — flagged here as needing native review.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
2026-05-11 00:05:20 +02:00
Paul Nothaft 15e333681f feat(settings): Features tab + sidebar reorg with feature-flag gating
Reorganises the admin sidebar around what users actually do, and adds a
single Features page that gates which feature surfaces appear in the
nav. Shrinks the main sidebar from 11 items to 4-6 (depending on
feature flags) and groups configuration screens into a single Settings
home with six logical sections.

Why
---
The current sidebar mixes three concerns: workspaces (Dashboard, Events,
Archives), feature surfaces (Analytics, Users), and configuration
screens that get touched maybe once a month (Email Settings, Branding,
Event Types, Backup, CMS Pages). That's 11 items, half of them config.

Backend
-------
- New `feature_flags` table (key, value, updated_at, updated_by).
  Migration 088 detects existing-vs-fresh installs from the events
  table:
    * Existing install (events>0)  → all 9 flags TRUE so nothing
      vanishes from an admin's UI on upgrade.
    * Fresh install      (events=0) → spec defaults: galleries,
      reminderEmails, analytics, userManagement TRUE; calendar,
      calendarBooking, quotes, bills, messaging FALSE.

- New `/api/admin/feature-flags` (GET/PUT) under `settings.view` and
  `settings.edit`. Server enforces the same dependency rules the
  frontend does (galleries always TRUE, quotes=false → bills=false,
  calendar=false → calendarBooking=false). PUT writes one
  `feature_flags_updated` activity log row with the diff.

Frontend
--------
- `FeatureFlagsContext` provides `useFeatureFlags()` (with staged/save/
  reset/isDirty) and `useFeatureEnabled(key)`. Mounted inside
  AdminLayout so flag fetches carry the auth cookie. Source of truth
  is the server response; staged is a local copy that the Features tab
  edits and the Save button PUTs.

- `RequireFeature` route guard for /admin/analytics and /admin/users —
  redirects to /admin/dashboard when the corresponding flag is OFF.

- AdminSidebar dropped from 11 to 6 items. Removed: Email Settings,
  Branding, Event Types, Backup, CMS Pages (now Settings tabs).
  Feature-gated: Analytics, Users.

- Old top-level routes (/admin/email, /admin/branding, /admin/event-
  types, /admin/backup, /admin/cms) kept as <Navigate> redirects to
  /admin/settings?tab=<key> so existing bookmarks don't 404.

- SettingsPage rewritten with a 6-group inner-nav (General /
  Content & Appearance / Communication / Privacy & Security /
  Integrations / System) and 19 tabs. New Features tab is the
  default landing tab. URL ?tab=<key> roundtrips with state — deep
  links and the back button work.

- FeaturesTab renders 9 cards across 5 sections. Toggles enabled for
  Analytics + User Management (the two flags that gate sidebar items
  in this PR). All other toggles disabled with a "Not yet available"
  lockedReason — the cards still render so admins see the roadmap, but
  the flag has no UI effect until the surface ships in its own PR. The
  galleries card is locked TRUE per spec (foundation, can't be off).

- Live SidebarPreview reflects unsaved staged changes — admins see
  what their sidebar will look like before they save.

- New i18n keys across all 5 locales (en, de, nl, pt, ru) for the
  Features tab copy, the new Settings group labels, and the lifted
  tab titles.

Verified end-to-end
-------------------
- Migration on this dev DB (existing install, 977 events): all 9 flags
  set to TRUE.
- Migration on simulated fresh install (events table emptied): spec
  defaults applied (5 OFF, 4 ON).
- Backend round-trip: GET → PUT → audit-log entry written, dependency
  rule enforced (bills forced false when quotes=false even when bills=
  true requested).
- UI Playwright spec: sidebar dropped 5 items, old top-level routes
  redirect, Features tab is default, Galleries+Calendar+Quotes+Bills+
  Messaging+ReminderEmails toggles disabled, Analytics+Users toggles
  enabled, toggling Analytics off + saving updates the sidebar +
  redirects /admin/analytics to /admin/dashboard.
- Smoke 13/13 still green; no regressions on existing flows.
2026-05-10 20:36:32 +02:00
PiR1 5fc427c74b feat(localization): update thumbnail settings and add fit options translations 2026-05-09 18:42:15 +02:00
PiR1 a5db4bd46e feat(translations): add French language support and improve localization handling 2026-05-09 18:23:11 +02:00
Paul Nothaft 1e69d5ff71 feat(webhooks): enrich event.* payloads with customer contact + share_token (#341)
The event.published webhook reporter wired into n8n to send WhatsApp
gallery links was missing the data needed to actually message the
customer — only event_name + share_url were in the payload, no
customer_name / customer_email / customer_phone, and no bare share
token to construct alternate URLs.

Adds a single canonical event subject helper (webhookService.buildEventSubject)
so every event.* webhook returns the same shape:

  { id, slug, event_name, event_type, event_date,
    share_url, share_token,
    customer_name, customer_email, customer_phone }

Fields the caller does not have in scope come back as null — keys are
always present so receivers do not have to distinguish "field missing"
from "field null". Pure addition: existing receivers continue to work,
existing templates ${data.event.event_name} keep working, and new
templates can now reference ${data.event.customer_phone} etc.

Wired into all five firing sites:
- routes/events.js — public event create (created + published)
- routes/adminEvents.js — admin create + draft→publish
- routes/v1/events.js — public v1 API (created + published)
- services/expirationChecker.js — event.expired (extra: expires_at)
- services/archiveService.js — event.archived (extra: archive_path)

PII surface area widens (customer email/phone now flow to webhook
receivers), so:
- Settings → Webhooks UI gets an amber Callout above the create form
  warning admins to only point webhooks at receivers they trust.
- Docs page updated with the new payload sample, the always-present
  null contract, and a Callout warning.

Verified end-to-end against the local dev webhook receiver — delivered
payload contains all 10 fields. webhookDelivery integration suite
remains 8/8 green.
2026-04-29 20:32:20 +02:00
Paul Nothaft c488f481ca feat: outbound webhooks for event/photo lifecycle (#327)
PicPeak POSTs lifecycle notifications to admin-configured URLs. Each
delivery is signed HMAC-SHA256 in the X-PicPeak-Signature header.
Verified end-to-end: 1/1 Playwright spec, 8/8 backend integration
tests, full UI click-through via Chrome DevTools.

Schema (migration 082)
- webhooks: id, name, url, secret (plaintext — required to compute HMAC
  for every outbound POST), secret_preview, events[], active, filter,
  template, created_by, timestamps, last_success_at/last_failure_at.
- webhook_deliveries: webhook_id (FK CASCADE), event_type, payload,
  attempt_count, status (pending|success|failed), response_status,
  response_body (truncated to 1KB), latency_ms, next_retry_at,
  last_error, created_at, completed_at. Composite index
  (status, next_retry_at) serves the worker's hot-path query.

Service + worker
- webhookService.fire(eventType, data) — non-throwing entry point used
  by lifecycle hooks. Looks up active webhooks subscribed to the event
  and applies their per-webhook filter (dot-path equality predicate)
  before enqueueing one webhook_deliveries row per match. Filter and
  template logic ship in this commit; admin surfaces in the follow-up.
- webhookDeliveryWorker — setInterval(5s) poller; fetches up to 5
  pending rows; per delivery: re-validates URL via networkValidation
  (DNS-rebinding mitigation, opt-out via WEBHOOK_ALLOW_PRIVATE_URLS),
  signs body with HMAC-SHA256, POSTs with 10s timeout, records outcome.
  Backoff schedule: 1m → 5m → 30m → 2h → 12h, max 5 attempts. Response
  body truncated to 1KB before storage. If a webhook has a template,
  the rendered string replaces the JSON envelope as the request body
  (signature is computed over the bytes actually sent).

Lifecycle wiring
- adminEvents.js POST /events → event.created (+ event.published when
  not draft); POST /:id/publish → event.published.
- routes/events.js (legacy public POST) → event.created + event.published.
- routes/v1/events.js (#322 API) → event.created + event.published on
  create, photo.uploaded on photo POST.
- archiveService.archiveEvent() → event.archived. Per-photo
  photo.deleted intentionally NOT fired during cascade — receivers
  infer from event.archived to avoid flooding (issue spec).
- expirationChecker.handleExpiredEvent() → event.expired BEFORE the
  cascading archive (so receivers see expired→archived in order).
- adminPhotos.js — photo.uploaded on each batch row, photo.deleted on
  single + bulk delete.
- photoProcessor.js — photo.uploaded for guest uploads + auto-import
  (covers all entry paths).
- fileWatcher.js — photo.uploaded on add, photo.deleted on unlink
  (local mode only).

Admin endpoints (mirrors adminApiTokens.js pattern)
- /api/admin/webhooks: GET list, POST create (returns plaintext secret
  exactly once), GET :id, PUT :id, DELETE :id, POST :id/test (synthetic
  fire), GET :id/deliveries (paginated, filter by status), GET
  :id/deliveries/:deliveryId, POST :id/deliveries/:deliveryId/replay.

Frontend
- Settings → Webhooks tab (mirrors API Tokens layout): name + URL +
  event checkboxes + "Advanced" expander for filter (JSON) and template.
  Plaintext secret shown once on creation with a Copy button. Active/
  Disabled toggle button per row.
- /admin/webhooks/:id/deliveries — operational debug surface. Table
  with timestamp/event/status/attempts/HTTP/latency. Status filter chips
  (all/pending/success/failed). Row click → slide-over with payload +
  signature + response body. Replay button on failed rows. Send-test-event
  dialog. Auto-refresh every 10s.

Dev infrastructure
- dev/webhook-receiver/ — tiny node:alpine HTTP server (~100 LOC) that
  records every POST to an in-memory ring buffer. Exposes GET /requests
  for the E2E spec to assert deliveries landed with the right HMAC.
  Sibling pattern to MinIO. Reachable from the backend at
  http://webhook-receiver:8888 inside the picpeak network.

Tests
- backend/__tests__/integration/webhookDelivery.test.js (8/8) —
  signature verification, headers, retry/backoff, max-attempts → failed,
  response truncation, disabled-mid-flight, SSRF block, start/stop
  idempotency.
- tests/e2e/webhooks-roundtrip.spec.ts (1/1) — create webhook → trigger
  event.published → assert receiver got POST with valid HMAC → visit
  deliveries page → row visible with status=success → API test event →
  API replay → disable webhook → assert no new delivery.

Docs
- README §"Webhooks" — event catalog, payload shape, HMAC verification
  in Node + Python + bash, retry semantics, SSRF protection.
- .env.example — WEBHOOK_ALLOW_PRIVATE_URLS, WEBHOOK_DELIVERY_INTERVAL_MS,
  WEBHOOK_DELIVERY_CONCURRENCY, WEBHOOK_HTTP_TIMEOUT_MS,
  WEBHOOK_MAX_ATTEMPTS.

Out of scope for v1 (per issue): webhook templates' code-eval (the
${dot.path} substitution that ships is pure string replacement, no
expression engine — see follow-up commit), per-webhook rate limiting
beyond the global concurrency cap, synchronous "ask before delete"
webhooks.

Spanning files
- App.tsx pulls in this commit with both the AnalyticsBootstrap
  (#325 dedup) and the WebhookDeliveriesPage route registration.
  Splitting via git add -p was forfeit for sanity; the single 92-line
  diff is honest about both contributions.
- adminEvents.js diff bundles the webhook fires AND the
  allow_presigned_download field plumbing (#328 follow-up). Same
  reasoning.
- The new webhookService/Worker/adminWebhooks files include the filter
  and template logic from the follow-up — they were authored in one
  pass; splitting them post-hoc would have produced fragile partial
  files. The follow-up commit covers the migration and the UI for these.
2026-04-28 10:07:39 +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 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 f50d7c0c51 feat: multilingual email templates with translations table
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
2026-04-04 17:43:01 +02:00
Paul NothaftandPaul Nothaft 486239aeb9 fix: address beta feedback - gallery layout fixes, Russian locale, email logo (#249)
- 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]>
2026-03-17 17:16:50 +01:00
Paul Nothaft 1fa222e9c4 feat: add photo cap per event and Portuguese (pt-BR) locale
- 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)
2026-03-16 17:22:54 +01:00
Paul Nothaft 28793bba68 Merge main into beta for release/beta-to-main 2026-03-11 20:12:52 +01:00
Paul Nothaft fc75bcdfc3 fix: video upload media type, select all, and dimension repair (#203, #220, #180)
- 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
2026-03-11 11:50:43 +01:00
Paul Nothaft 9b7495e005 Merge pull request #214 from the-luap/feat/configurable-upload-batch-size
feat: configurable upload batch size for reverse proxy compatibility
2026-03-05 22:21:29 +01:00
Paul Nothaft cc4503ad28 Revert "feat: configurable upload batch size for reverse proxy compatibility" 2026-03-05 22:16:28 +01:00
Paul Nothaft 424336340b Merge pull request #210 from the-luap/feat/configurable-upload-batch-size
feat: configurable upload batch size for reverse proxy compatibility
2026-03-05 22:14:41 +01:00
Paul Nothaft 02a46e083d feat: add configurable upload batch size for reverse proxy compatibility (#208)
Users behind Cloudflare Tunnel and other reverse proxies cannot upload
batches >100MB. The upload chunking previously used a hardcoded 500MB
limit. This adds a configurable `max_upload_batch_size_mb` setting
(default 95MB) to the admin General settings, leaving headroom below
Cloudflare's 100MB limit.
2026-03-05 22:12:37 +01:00
Paul Nothaft 7d6d2f5688 feat: add thumbnail settings UI to admin settings page (#206)
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.
2026-03-04 22:55:14 +01:00
Paul Nothaft 33483cf32d fix: resolve issues #194, #195, #196, #197
- #194: Send full date format object instead of just format string to prevent JSON parse errors
- #195: Remove non-functional forgot password link, fix README port 3005 -> 3000
- #196: Use ADMIN_PASSWORD env var in migration, update existing user in create-admin script instead of failing
- #197: Convert camelCase filter keys to snake_case in photo export to match backend PhotoFilterBuilder
2026-02-22 22:34:44 +01:00
Paul Nothaft 50c09904a9 feat: add update instructions dialog, email notifications, and capture date sorting
- 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
2026-02-16 16:23:57 +01:00
Paul Nothaft 9c2a0d272a feat: add admin dark mode and SEO/robots.txt settings
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
2026-02-06 23:26:01 +01:00
Paul Nothaft d4a15dbe74 fix: remove non-functional watermark toggle from Feature Toggles
The "Enable watermark on photos" checkbox in Settings > General > Feature
Toggles was not connected to any backend logic - it stored a setting that
was never read or used. The actual working watermark functionality exists
in Settings > Branding.

This removes the dead toggle to eliminate user confusion (fixes #140).
2026-01-22 13:54:23 +01:00
Paul Nothaft 2151147f2d feat: add optional event date and expiration settings
Add global settings to make event_date and expiration optional when
creating galleries. This supports non-event use cases like portraits,
corporate shoots, etc.

New features:
- Settings toggles in Settings → Event Creation tab
- "Require event date" checkbox with warning about random URL identifiers
- "Require expiration date" checkbox with warning about manual archiving
- Galleries without date use random hex suffix in slug (e.g. portrait-smith-a1b2c3)
- Galleries without expiration never expire (stay active until archived)

Backend changes:
- New migration for settings and nullable columns
- Conditional validation based on settings
- Updated slug generation with random suffix fallback
- Updated expiration checker to skip null expires_at
- Updated gallery access control for null expiration

Frontend changes:
- New checkboxes in EventsTab with warnings
- Conditional event date field (shows optional label)
- No Expiration message when expiration disabled
- Updated types for nullable event_date and expires_at

Closes #118
2026-01-16 09:39:32 +01:00
Paul Nothaft 892e47d017 feat: add multi-administrator support with RBAC and fix backup/restore for S3
## Multi-Administrator System
- Add role-based access control (RBAC) with predefined roles (Super Admin, Admin, Editor, Viewer)
- Add granular permissions system for all admin operations
- Add admin user management page with invite functionality
- Add email invitation system for new administrators
- Add permission middleware protecting all admin routes
- Add PermissionGate component for frontend permission checks
- Track event creator (created_by) for audit purposes

## Backup & Restore Fixes
- Fix S3 backup: endpoint URL handling, manifest loading, field name compatibility
- Fix S3 restore: add list-backups endpoint, transform S3 config from frontend format
- Fix PostgreSQL compatibility: add .returning('id') for insert operations
- Fix disk space check: use df command, handle unknown space gracefully
- Fix dry-run validation to not block on warnings
- Fix req.user → req.admin in restore routes

## Database Migrations
- 054: Add roles table with predefined roles
- 055: Add permissions table
- 056: Add role_permissions junction table
- 057: Add role_id to admin_users
- 058: Add admin_invitations table
- 059: Add admin email templates
- 060: Add created_by to events table

## Other Improvements
- Update .gitignore to exclude planning docs and local backup directory
- Remove SQLite database file from tracking
- Add i18n translations for user management (EN/DE)
2026-01-07 17:10:46 +01:00
Paul Nothaft 6033461be1 feat: add Apple Liquid Glass templates, image security settings, and automated releases
## New Features
- Apple Liquid Glass CSS template with iOS 26-inspired design
- Liquid Glass Dark theme with neon accents
- Image Security settings tab with per-event protection levels
- Release Please automation for versioning and changelog

## Improvements
- Update CSS template migration with final working templates
- Add search placeholder visibility fix for glass themes
- Update README roadmap (Download Protection, Gallery Templates, Filtering & Export now implemented)

## Infrastructure
- Add release-please.yml workflow for automated releases
- Add release-please-config.json and manifest
- Update docker-build.yml with Release Please integration comments
- Add comprehensive CHANGELOG.md

## Cleanup
- Add working/planning docs to .gitignore (CLAUDE.md, test-*.md, feature-*.md, etc.)
- Remove internal planning documents from git tracking (kept locally)

## Files Added
- .github/workflows/release-please.yml
- .release-please-manifest.json
- release-please-config.json
- CHANGELOG.md
- frontend/src/features/settings/tabs/ImageSecurityTab.tsx
2026-01-03 23:35:23 +01:00
Paul Nothaft 0da45e699a feat: Add CSS template system with custom gallery styling support
## Changes

### CSS Template System
- Added CSS class hooks to gallery components for custom template targeting
- Gallery sidebar, header, footer, and photo cards can now be styled via CSS templates
- CSS variables on :root allow themes to override colors, effects, and spacing

### Gallery Component CSS Classes Added
- `.gallery-page` - Main gallery container
- `.gallery-header` - Top header bar
- `.gallery-sidebar` - Filter/download sidebar
- `.gallery-sidebar-header`, `.gallery-sidebar-title`, `.gallery-sidebar-close`
- `.gallery-sidebar-content`, `.gallery-sidebar-section`
- `.gallery-sidebar-search-input`, `.gallery-sidebar-search-icon`
- `.gallery-sidebar-backdrop` - Mobile overlay
- `.gallery-btn`, `.gallery-btn-download` - Sidebar buttons
- `.gallery-footer` - Footer section
- `.photo-card`, `.photo-grid` - Photo display elements

### CSS Templates (Database)
- Elegant Dark (id=1): Dark navy theme with light text and red accents
- Liquid Glass Light (id=2): iOS 26 frosted glass effect with gradient background

### Bug Fixes
- Fixed CSS variables not inheriting (moved from .gallery-page to :root)
- Fixed sidebar position breaking layout (removed position: relative override)
- Fixed Elegant Dark sidebar text visibility (white on white issue)

### Other Changes
- Settings page refactoring and cleanup
- i18n locale updates for new gallery features
- Vite proxy port configuration fix
- Admin auth route improvements
- CSS templates service updates
2026-01-03 08:59:01 +01:00