Commit Graph

1705 Commits

Author SHA1 Message Date
Paul Nothaft 6850bd3bef chore(main): release 3.78.0-beta.0 (#725)
Build and Push Docker Images / build-backend (linux/amd64, ubuntu-latest) (push) Has been cancelled
Build and Push Docker Images / build-backend (linux/arm64, ubuntu-24.04-arm) (push) Has been cancelled
Build and Push Docker Images / merge-backend (push) Has been cancelled
Build and Push Docker Images / build-frontend (linux/amd64, ubuntu-latest) (push) Has been cancelled
Build and Push Docker Images / build-frontend (linux/arm64, ubuntu-24.04-arm) (push) Has been cancelled
Build and Push Docker Images / merge-frontend (push) Has been cancelled
Build and Push Docker Images / summary (push) Has been cancelled
v3.78.0-beta.0
2026-07-02 14:35:07 +00:00
Paul Nothaft 97b9853709 Merge pull request #724 from PicPeak/fix/release-automerge-pat
fix: enable release-PR auto-merge with the PAT so releases actually publish
2026-07-02 16:32:04 +02:00
Paul Nothaft bafc96f468 Merge pull request #714 from Luca-Timo/feat/first-run-setup-wizard
feat: zero-config first run — in-browser admin bootstrap + auto-generated secrets
2026-07-02 16:19:33 +02:00
Paul Nothaft e08a33d9ea fix(ci): enable release-PR auto-merge with the PAT, not GITHUB_TOKEN
Auto-merge enabled via GITHUB_TOKEN attributes the eventual merge commit to
github-actions[bot], so recursion prevention suppresses the resulting push to
main — the follow-up release-please run that cuts the tag/release never fires.
Net: the version PR merges but no release/tag/images are ever produced (#719).

Enable auto-merge with RELEASE_PLEASE_TOKEN instead (a real identity) so the
merge triggers the tag-cutting run. Approval stays on GITHUB_TOKEN because it
must be a different identity than the PR author (the PAT) to count as a review.

Observed on #723: merged 3.77.3-beta.0 but no run followed and no tag was cut.
2026-07-02 15:41:25 +02:00
Paul Nothaft 2b2dc6e35c chore(main): release 3.77.3-beta.0 (#723)
Build and Push Docker Images / build-backend (linux/amd64, ubuntu-latest) (push) Has been cancelled
Build and Push Docker Images / build-backend (linux/arm64, ubuntu-24.04-arm) (push) Has been cancelled
Build and Push Docker Images / merge-backend (push) Has been cancelled
Build and Push Docker Images / build-frontend (linux/amd64, ubuntu-latest) (push) Has been cancelled
Build and Push Docker Images / build-frontend (linux/arm64, ubuntu-24.04-arm) (push) Has been cancelled
Build and Push Docker Images / merge-frontend (push) Has been cancelled
Build and Push Docker Images / summary (push) Has been cancelled
v3.77.3-beta.0
2026-07-02 13:29:08 +00:00
Paul Nothaft d00d52a221 Merge pull request #722 from PicPeak/fix/release-please-gh-repo
fix: set GH_REPO in release-please auto-merge step
2026-07-02 15:23:49 +02:00
Paul Nothaft e54100ac46 Merge pull request #721 from PicPeak/release-please--branches--main
Build and Push Docker Images / build-backend (linux/amd64, ubuntu-latest) (push) Has been cancelled
Build and Push Docker Images / build-backend (linux/arm64, ubuntu-24.04-arm) (push) Has been cancelled
Build and Push Docker Images / merge-backend (push) Has been cancelled
Build and Push Docker Images / build-frontend (linux/amd64, ubuntu-latest) (push) Has been cancelled
Build and Push Docker Images / build-frontend (linux/arm64, ubuntu-24.04-arm) (push) Has been cancelled
Build and Push Docker Images / merge-frontend (push) Has been cancelled
Build and Push Docker Images / summary (push) Has been cancelled
chore(main): release 3.77.2-beta.0
v3.77.2-beta.0
2026-07-02 15:23:08 +02:00
Paul Nothaft 0cab43ed89 fix(ci): set GH_REPO in release-please auto-merge step
The auto-merge step runs in a job with no actions/checkout, so gh could not
infer the repository from a git remote and failed with 'not a git repository'
(#719 follow-up). Set GH_REPO=${{ github.repository }} so gh pr list/review/
merge work without a checkout — same fix as the whatsnew workflow (2a5f0a8).

Confirmed working otherwise: with RELEASE_PLEASE_TOKEN set, release PR #721 is
now PAT-authored and its required checks run automatically (no manual approval).
2026-07-02 15:20:07 +02:00
Luca b0912c7427 feat(setup): validate setup token at step 1 before advancing
Previously "Continue" on the token step only checked the field was
non-empty; a wrong token wasn't caught until the final submit, after the
user had filled in email + password. Add a non-burning verify:

- backend: POST /setup/verify-token constant-time compares the token
  without consuming it (createInitialAdmin still claims it atomically on
  submit), gated on no-admin-exists and rate-limited like /setup/admin.
- frontend: step-1 "Continue" calls verifyToken and only advances on a
  valid token; a wrong token shows the invalidToken error on the field,
  429 -> too-many-attempts, 409 -> redirect to login.

Adds integration tests for accept-without-burn / reject / closed-once-set.
2026-07-02 15:18:22 +02:00
Paul Nothaft 95dac43fdf chore(main): release 3.77.2-beta.0 2026-07-02 15:17:15 +02:00
Paul Nothaft fb64ec0910 Merge pull request #720 from PicPeak/fix/release-please-automation
fix: auto-publish release-please PRs without manual approval
2026-07-02 15:16:54 +02:00
Luca 3e69c5df3f fix(setup): match first-run logo size to the login page default
The header logo used a hardcoded 64px frame; the login page renders a
medium (200x150) frame via resolveLoginLogoClasses. Reuse that helper
with the default size so /setup and /admin/login read identically.
2026-07-02 15:03:56 +02:00
Paul Nothaft a3e7232b8e fix(ci): auto-publish release-please PRs without manual approval (#719)
The release PR (authored by github-actions[bot] via GITHUB_TOKEN) sat open
forever: its workflows were held behind 'awaiting approval' and the required
review could not be satisfied by the bot. Both release-please workflows now:

- Use a dedicated ${{ secrets.RELEASE_PLEASE_TOKEN }} (fine-grained PAT) with a
  GITHUB_TOKEN fallback. A PAT-authored PR runs CI automatically (no 'awaiting
  approval') and can be merged without a human.
- Auto-approve (as github-actions[bot], a different identity than the PR
  author) and enable auto-merge on the open release PR, so it publishes once
  checks pass. Skipped when no PAT is configured — falls back to today's manual
  flow, nothing breaks.

A PAT also un-suppresses the tag-push and release-published triggers on
docker-build (GITHUB_TOKEN suppressed them), so add a concurrency group there
to collapse the duplicate same-version builds into one.

Requires (repo/org settings, one-time):
- Create fine-grained PAT RELEASE_PLEASE_TOKEN (contents:write, pull-requests:write).
- Enable 'Allow auto-merge' on the repo (currently off).
- 'Allow GitHub Actions to approve pull requests' — already enabled.
2026-07-02 15:01:52 +02:00
Luca d9b0eb7232 feat(setup): brand first-run screen and split into two-step wizard
Address post-merge UI feedback on the first-run setup screen — the first
screen any new admin sees:

- Use the bundled PicPeak logo (same asset the login page falls back to)
  on the cream brand plate instead of the generic lucide Sparkles icon.
- Split the flow into two steps: step 1 takes only the one-time setup
  token, with the `docker compose logs backend | grep -i "setup token"`
  recovery command shown prominently (with a copy button) directly under
  the field, plus a docs link for when the logs have rotated away; step 2
  collects email + password. A rejected token bounces back to step 1.

en/de strings added; other locales fall back to en.
2026-07-02 14:56:14 +02:00
Paul Nothaft 934cc92bb8 Merge pull request #717 from PicPeak/release-please--branches--main
Build and Push Docker Images / build-backend (linux/amd64, ubuntu-latest) (push) Has been cancelled
Build and Push Docker Images / build-backend (linux/arm64, ubuntu-24.04-arm) (push) Has been cancelled
Build and Push Docker Images / merge-backend (push) Has been cancelled
Build and Push Docker Images / build-frontend (linux/amd64, ubuntu-latest) (push) Has been cancelled
Build and Push Docker Images / build-frontend (linux/arm64, ubuntu-24.04-arm) (push) Has been cancelled
Build and Push Docker Images / merge-frontend (push) Has been cancelled
Build and Push Docker Images / summary (push) Has been cancelled
chore(main): release 3.77.1-beta.0
v3.77.1-beta.0
2026-07-02 14:51:09 +02:00
Luca 286975dc52 fix(setup): address PR #714 review — password UX, script token, race, nits
Blockers:
- SetupPage now mirrors the server password rule (>=8 with upper/lower/digit) so
  a green client isn't bounced by the server; server errors carry a `field`
  (routes/setup.js) that the client maps to a translated key instead of
  rendering raw English. New i18n: setup.invalidToken, setup.passwordRequirements.
- picpeak-setup.sh: the ADMIN_CREDENTIALS.txt block no longer dead-ends on the
  wizard path — when no legacy admin was seeded it prints the one-time setup
  token (from data/SETUP_TOKEN / docker compose logs) and points at /setup.

Concern:
- createInitialAdmin creates the admin + burns the token in ONE transaction,
  atomically claiming the token (null-if-present, expect 1 row) so a
  double-submit can't create two super_admins. Cross-DB (whereNotNull, trx-only
  writes). Added a concurrency test.

Nits:
- SetupPage redirects to /login when /setup/status errors (no form flash on a
  configured instance).
- Dropped the unused DATABASE_URL from docker-compose.yml.
- Documented why secrets are chmod 644 (three different reader users).
2026-07-02 13:21:55 +02:00
github-actions[bot] 6f5a02b817 chore(main): release 3.77.1-beta.0 2026-07-02 11:01:34 +00:00
Paul Nothaft f5b4aa7a5b Merge pull request #716 from PicPeak/docs/require-ui-screenshots
docs: require screenshots for UI changes in PRs
2026-07-02 13:01:04 +02:00
Paul Nothaft 8ca74776f4 docs: require screenshots for UI changes in PRs
Any PR that changes a user-facing surface must include a screenshot of the
result in the description (before/after where it helps). Reviewers ask for
one before reviewing UI-touching PRs; backend/non-visual changes are exempt.
2026-07-02 11:57:32 +02:00
Luca 415bffa04c feat: zero-config first run — in-browser admin bootstrap + auto-generated secrets
Fresh installs need nothing in .env. See PR description for the full feature.
2026-07-01 14:49:18 +02:00
Paul Nothaft b8b33ae6d6 Merge pull request #713 from PicPeak/release-please--branches--main
Build and Push Docker Images / build-backend (linux/amd64, ubuntu-latest) (push) Has been cancelled
Build and Push Docker Images / build-backend (linux/arm64, ubuntu-24.04-arm) (push) Has been cancelled
Build and Push Docker Images / merge-backend (push) Has been cancelled
Build and Push Docker Images / build-frontend (linux/amd64, ubuntu-latest) (push) Has been cancelled
Build and Push Docker Images / build-frontend (linux/arm64, ubuntu-24.04-arm) (push) Has been cancelled
Build and Push Docker Images / merge-frontend (push) Has been cancelled
Build and Push Docker Images / summary (push) Has been cancelled
chore(main): release 3.77.0-beta.0
v3.77.0-beta.0
2026-07-01 10:00:29 +02:00
github-actions[bot] e1dae31e16 chore(main): release 3.77.0-beta.0 2026-07-01 07:55:55 +00:00
Paul Nothaft e873f7c98c Merge pull request #712 from PicPeak/chore/conventional-pr-title-guard
feat: admin photos list/grid toggle + upload failure report (#707, #708)
2026-07-01 09:55:37 +02:00
Paul Nothaft 6f95796b7c feat: admin photos list/grid toggle + upload failure report (#707, #708)
Records two features that merged with gitmoji commit subjects and were
therefore skipped by Release Please, so the next beta credits them:
- #707 grid/list layout toggle on the admin Photos tab
- #708 per-file upload failure report in the upload modal

No code change — the features are already on main; this commit only gives
Release Please a Conventional Commit to cut the release from.
2026-07-01 09:38:34 +02:00
Paul Nothaft 4881d2040a ci: validate PR titles against Conventional Commits
Release Please only recognizes Conventional Commit prefixes (feat:, fix:,
...). PRs merged with other conventions (gitmoji, free-form) are silently
skipped, shipping changes with no version bump or changelog entry (see
#707/#708). Fail such PRs early via amannn/action-semantic-pull-request.
2026-07-01 09:38:34 +02:00
Paul Nothaft b8665e1d86 Merge pull request #708 from andredlng/feat/upload-failure-details
Surface which files failed during photo upload
2026-07-01 09:29:19 +02:00
Paul Nothaft f4a1db8b6a Merge pull request #707 from andredlng/feat/photos-list-view
Add grid/list layout toggle to admin Photos tab
2026-07-01 09:00:31 +02:00
André Deuerling 1be871cb9a 🐛 Address review: fix spinner hang + show processing failures
Blocker: the "every file rejected" reset never fired because the backend
returns `upload_id` unconditionally (with count 0), so `anyQueued` was
always true and the completion effect (gated on total > 0) never ran —
modal spun forever. Gate `anyQueued` on `count > 0` so a zero-photo
response takes the terminal reset path.

Concern 1: processing-stage failures were invisible — the modal
auto-closed on clean transfer before the worker reported them. Defer the
settle/close decision to the completion effect (combining transfer +
processing failures), and persist failed photos into `processingFailures`
state before `uploadIds` is cleared, so the rows don't vanish the instant
they appear.

Also: report card gets role="status"/aria-live (nit), and tests now cover
the whole-chunk transfer failure, the clean-settle path, and the
onUploadSettled contract from the real component.
2026-07-01 08:03:01 +02:00
André Deuerling 2a81d992f1 🎨 Address review: persist-on-click + radiogroup toggle
- Persist the layout choice in the toggle click handlers instead of a
  useEffect, so simply opening the Photos tab no longer re-writes the
  value it just read from localStorage (review concern 1).
- Give the Grid/List toggle radiogroup/radio + aria-checked semantics
  so a screen reader announces them as one mutually-exclusive set
  (review concern 2).
- Add a test that mount performs no localStorage write.
2026-07-01 07:55:49 +02:00
Paul Nothaft 22b20e8314 Merge pull request #711 from PicPeak/release-please--branches--main
Build and Push Docker Images / build-backend (linux/amd64, ubuntu-latest) (push) Has been cancelled
Build and Push Docker Images / build-backend (linux/arm64, ubuntu-24.04-arm) (push) Has been cancelled
Build and Push Docker Images / merge-backend (push) Has been cancelled
Build and Push Docker Images / build-frontend (linux/amd64, ubuntu-latest) (push) Has been cancelled
Build and Push Docker Images / build-frontend (linux/arm64, ubuntu-24.04-arm) (push) Has been cancelled
Build and Push Docker Images / merge-frontend (push) Has been cancelled
Build and Push Docker Images / summary (push) Has been cancelled
chore(main): release 3.76.2-beta.0
v3.76.2-beta.0
2026-06-30 22:57:16 +02:00
github-actions[bot] 79924e10d9 chore(main): release 3.76.2-beta.0 2026-06-30 20:52:33 +00:00
Paul Nothaft 3feed0fae6 Merge pull request #710 from PicPeak/fix/whatsnew-workflow-no-checkout
fix(ci): whatsnew highlights — set GH_REPO so gh runs without a checkout
2026-06-30 22:52:08 +02:00
Paul Nothaft 2a5f0a8601 fix(ci): whatsnew highlights — set GH_REPO so gh runs without a checkout
Release Please Beta on v3.76.1-beta.0 hard-failed at the very first
`gh release view "$TAG"` call:

  failed to run git: fatal: not a git repository
  (or any of the parent directories): .git

The reusable `whatsnew-highlights.yml` (PR #703) doesn't run
actions/checkout — so when `gh` tried to infer the target repo from
the runner's empty workspace it errored out. The first time it ran
against an actual release (#709 → 3.76.1-beta.0), the whole job died
before the deterministic-fallback path could save it.

Two changes, both single-line:

1. `env.GH_REPO: ${{ github.repository }}` at job scope. `gh` honours
   this and won't fall back to parsing `.git/config`, so no checkout
   is needed (the workflow only calls the GitHub API, never reads
   repo files).

2. `continue-on-error: true` on the "Extract Features" step. The
   file's comments say "never let highlights break a release", but
   the original wiring only soft-failed the AI + inject steps. A
   transient API hiccup at extract still hard-failed the whole job —
   defeating the design intent. Match the comment.

Why not just add actions/checkout? It would work, but pulls the whole
repo over the wire on every release just for `gh` to read its own
config. GH_REPO is the lighter idiom.

Net impact today: v3.76.1-beta.0 shipped without the `<!-- whatsnew -->`
block; the app's parseWhatsNew() already falls back to the raw Features
list so the admin "What's New" banner still works. The next beta release
will pick up the polished version.
2026-06-30 22:47:10 +02:00
Paul Nothaft 152952877f Merge pull request #709 from PicPeak/release-please--branches--main
Build and Push Docker Images / build-backend (linux/amd64, ubuntu-latest) (push) Has been cancelled
Build and Push Docker Images / build-backend (linux/arm64, ubuntu-24.04-arm) (push) Has been cancelled
Build and Push Docker Images / merge-backend (push) Has been cancelled
Build and Push Docker Images / build-frontend (linux/amd64, ubuntu-latest) (push) Has been cancelled
Build and Push Docker Images / build-frontend (linux/arm64, ubuntu-24.04-arm) (push) Has been cancelled
Build and Push Docker Images / merge-frontend (push) Has been cancelled
Build and Push Docker Images / summary (push) Has been cancelled
chore(main): release 3.76.1-beta.0
v3.76.1-beta.0
2026-06-30 22:42:09 +02:00
github-actions[bot] 753e680b1c chore(main): release 3.76.1-beta.0 2026-06-30 20:33:22 +00:00
Paul Nothaft 0b74b51f40 Merge pull request #703 from Luca-Timo/ci/whatsnew-highlights-workflow
ci(whatsnew): generate release highlights via GitHub Models
2026-06-30 22:33:00 +02:00
André Deuerling cc26296c58 🐛 Keep upload modal open when some files fail
The failure report lives inside the upload modal, but the modal
auto-closed the instant the transfer finished (handleUploadComplete →
onClose), unmounting the report before the user could read it — so the
"which files failed" list never actually appeared.

Split the modal's completion callback in two:
- onUploadComplete: refresh the grid only (no close), as bytes land and
  again when processing finishes
- onUploadSettled({ hasFailures }): fired once the transfer settles; the
  modal auto-closes only on a clean upload and stays open (report
  visible) when any file failed

Also reset the transfer UI when nothing was queued (every file failed),
which previously left the modal spinning forever. Add a PhotoUploadModal
test covering close-on-clean vs stay-open-on-failure.
2026-06-30 21:22:49 +02:00
André Deuerling 1b0100cfad Surface which files failed during photo upload
A partial upload only told the admin "some files failed" with no way to
find out which ones — even though the data existed. The backend already
returns per-file rejections (response.errors: [{filename, error}]) and the
progress hook already exposes failedPhotos, but both were dropped.

Add a dismissible failure report to the upload modal listing every file
that didn't make it into the gallery, grouped by stage with its reason:
- rejected: per-file validation rejections from the upload response
  (previously discarded entirely)
- transfer: whole-chunk request failures (now captured with the error,
  not just the filename)
- processing: background-worker failures from useUploadProgress.failedPhotos

Replace the count-only "some files failed" toast with one that points at
the list. Add en/de keys under upload.failures.* and a component test
covering the rejected + processing rows and dismissal.
2026-06-30 21:08:02 +02:00
André Deuerling 46ce59d82e Add grid/list layout toggle to admin Photos tab
The event detail Photos tab (AdminPhotoGrid) only offered a thumbnail
grid. Add a Grid/List toggle in the action bar so admins can scan
photos in a compact, metadata-oriented list.

- New utils/photoViewPrefs.ts persists the choice per admin via
  localStorage (mirrors utils/calendarPrefs.ts), defaulting to grid
- List view is a compact <table> following the established admin
  list pattern (EventsListPage), with responsive column hiding:
  Photo (thumbnail + filename + original + Video/Hidden badges),
  Category (lg+), Uploaded date (md+, via useLocalizedDate),
  Engagement views/downloads/likes (xl+), Feedback rating/comments
  (sm+), Size, and hover Actions (download, delete)
- Rows reuse the existing selection, download, delete and category
  handlers; row click opens the photo viewer
- Toggle buttons use LayoutGrid / List icons with aria-pressed state
- Add en.json + de.json keys under admin.photos (viewMode, gridView,
  listView, columns.*)
- Tests for the persistence util and the toggle's render + persistence
2026-06-30 19:17:26 +02:00
Luca 5582644dc4 fix(whatsnew): decode HTML entities and trim em-dash detail in fallback bullets
The Features-fallback showed raw changelog text, so a commit subject like
'branded URL shortener — /s/<slug> with OG injection' surfaced two problems
in the admin banner:
- release-please escapes <slug> to &lt;slug&gt;; React renders the literal
  entity, so the banner read '/s/&lt;slug&gt;'. Decode the entities
  (&lt; &gt; &amp; &quot; &#39;), &amp; last to avoid double-decoding.
- the technical tail leaked into a user-facing highlight. Drop a trailing
  '— detail' clause (em dash only, so 'mark-paid' is untouched) so the bullet
  reads as the headline 'branded URL shortener'.

Only affects the deterministic fallback; curated <!-- whatsnew --> blocks are
unchanged.
2026-06-30 18:39:21 +02:00
Luca d25178d2e5 ci(whatsnew): let the Models step fail soft so the fallback runs without Models
If GitHub Models is disabled for the org the ai-inference step errors;
without continue-on-error the job would go red and skip the inject+fallback.
Mark it continue-on-error so an unavailable Models cleanly degrades to the
deterministic bullets — the feature now works with Models off, not just on.
2026-06-30 17:28:31 +02:00
Paul Nothaft 627c655a4d Merge pull request #704 from PicPeak/release-please--branches--main
Build and Push Docker Images / build-backend (linux/amd64, ubuntu-latest) (push) Has been cancelled
Build and Push Docker Images / build-backend (linux/arm64, ubuntu-24.04-arm) (push) Has been cancelled
Build and Push Docker Images / merge-backend (push) Has been cancelled
Build and Push Docker Images / build-frontend (linux/amd64, ubuntu-latest) (push) Has been cancelled
Build and Push Docker Images / build-frontend (linux/arm64, ubuntu-24.04-arm) (push) Has been cancelled
Build and Push Docker Images / merge-frontend (push) Has been cancelled
Build and Push Docker Images / summary (push) Has been cancelled
chore(main): release 3.76.0-beta.0
v3.76.0-beta.0
2026-06-30 17:12:31 +02:00
github-actions[bot] 6439cf2e42 chore(main): release 3.76.0-beta.0 2026-06-30 15:02:34 +00:00
Paul Nothaft a0f7033ffc Merge pull request #702 from PicPeak/feat/branded-short-urls-699
feat(gallery): branded URL shortener — /s/<slug> with OG injection (#699)
2026-06-30 17:01:16 +02:00
Paul Nothaft 3ac88370d6 Merge pull request #701 from PicPeak/release-please--branches--main
Build and Push Docker Images / build-backend (linux/amd64, ubuntu-latest) (push) Has been cancelled
Build and Push Docker Images / build-backend (linux/arm64, ubuntu-24.04-arm) (push) Has been cancelled
Build and Push Docker Images / merge-backend (push) Has been cancelled
Build and Push Docker Images / build-frontend (linux/amd64, ubuntu-latest) (push) Has been cancelled
Build and Push Docker Images / build-frontend (linux/arm64, ubuntu-24.04-arm) (push) Has been cancelled
Build and Push Docker Images / merge-frontend (push) Has been cancelled
Build and Push Docker Images / summary (push) Has been cancelled
chore(main): release 3.75.1-beta.0
v3.75.1-beta.0
2026-06-30 17:00:10 +02:00
Luca 5aeb6905ac ci(whatsnew): generate release highlights via GitHub Models
Activate the What's New highlights step that condenses each release's
Features into <=8 short bullets and injects a <!-- whatsnew --> block the
app reads (utils/whatsNew.parseWhatsNew), with a deterministic fallback.

Runs as a needs: job inside the release-please workflows rather than on a
standalone release: published trigger, because release-please creates the
release with GITHUB_TOKEN and GitHub never starts new workflow runs from
token-generated events -- a standalone trigger would never fire. Shared as
a reusable workflow_call so the stable and beta channels stay in sync.

Best-effort: continue-on-error + fallback mean it can never break a release.
Requires GitHub Models enabled for the org; until then the fallback is used.
2026-06-30 16:43:54 +02:00
Paul Nothaft 56c2386c90 feat(gallery): branded URL shortener — /s/<slug> with OG injection (#699)
Issue 3 from #699 (@alexvaltchev's report): expose a custom-named short
URL per event that bots scrape for OG previews and browsers redirect to
the underlying gallery. WhatsApp / iMessage / Facebook cache the OG
metadata by the URL they crawl, so the SHORT URL becomes the cache key
— admins can rotate or split-test underlying gallery URLs without
re-pushing a fresh link to clients.

Additive feature; no existing route, table, or column is modified.

## Backend

- `gallery_short_urls` table (migration 150): id, short_slug UNIQUE,
  event_id FK CASCADE, target_path TEXT, created_by/at, hit_count,
  last_hit_at, deleted_at/by. hasTable-guarded so the migration is
  idempotent on re-run.

- `src/services/galleryShortUrlService.js` — validator + CRUD +
  resolver. Slug rules: `/^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$/`,
  reserved blocklist (admin, api, auth, gallery, og, s, login, ...).
  target_path snapshots at create-time from the event + global
  short-URL toggle, so a later flip of the toggle does NOT silently
  change where existing short URLs resolve.

- `src/routes/adminShortUrls.js` — `GET/POST
  /api/admin/events/:eventId/short-urls`, `DELETE
  /api/admin/short-urls/:id`. Structured errors: 400 INVALID_SLUG,
  409 SLUG_TAKEN (with `suggested`), 404 EVENT_NOT_FOUND. Gated by
  events.view / events.edit + requireEventOwnership.

- `server.js` /s/:shortSlug public route. Bot UA → server-render the
  same OG metadata the existing /og/gallery/<slug> handler produces,
  then override og:url to point at /s/<shortSlug> itself (cache-key
  invariant — social platforms key by the URL they scrape).
  Browser UA → 302 to target_path. Soft-deleted slug → 410 Gone
  (intentional-delete signal, distinct from 404 unknown slug).
  Hit accounting is fire-and-forget.

## Frontend

- `services/shortUrls.service.ts` — list/create/remove.
- `components/admin/ShortUrlsCard.tsx` — per-event card on the
  EventDetailsPage. Form for custom or auto-generated slug, list with
  copy-to-clipboard + soft-delete. SLUG_TAKEN error surfaces the
  service's `suggested` slug with a "use suggested" button.
- i18n: events.shortUrls.* added to EN + DE.

## Tests

78 new tests, all passing:

- `__tests__/utils/galleryShortUrlValidation.test.js` (48) — pure-
  function tests for validateSlug: accepts/rejects, reserved-slug
  blocklist, path-traversal + URL-injection vectors.
- `__tests__/integration/galleryShortUrls.test.js` (19) — service
  layer against a real SQLite DB. Covers custom + auto-generated
  slugs, collision + SLUG_TAKEN + suggested, target_path
  snapshotting (backward-compat invariant), soft-delete + slug
  rotation, hit counting.
- `__tests__/integration/galleryShortUrlRoute.test.js` (11) —
  HTTP-level: 302 redirect for browser UA, 200 + OG HTML for bot UA,
  og:url canonical points at /s/<slug>, 410 for soft-deleted +
  orphaned events, 404 unknown + malformed.

Regression sweep: 47 existing migration-chain integration tests still
pass; migration 150 is additive only.

## Backward compatibility

- Existing `/gallery/<slug>`, `/gallery/<32-hex-share-token>`,
  `/gallery/<slug>/show/<token>`, `/og/gallery/<slug>`,
  `/og/gallery/<slug>/cover` routes are untouched.
- The `/s/` namespace is new; no existing route lives there.
- Migration 150 only ADDs the new table — no ALTERs on existing
  schema, no destructive changes.
- target_path is snapshotted at create-time so flipping the global
  "Use short gallery URLs" setting after a short URL exists does NOT
  change where that short URL resolves.
2026-06-30 16:30:13 +02:00
github-actions[bot] 541b3d32ef chore(main): release 3.75.1-beta.0 2026-06-30 14:09:58 +00:00
Paul Nothaft 25bf7bb523 Merge pull request #700 from PicPeak/fix/og-injection-share-token-and-slideshow-699
fix(og): rich social previews for share-token + slideshow URLs (#699)
2026-06-30 16:09:31 +02:00
Paul Nothaft 1b8747dc82 fix(og): rich social previews for share-token + slideshow URLs (#699)
Two SSR-OG injection bugs reported by @alexvaltchev. Both made his link
previews fall back to the brand logo + site-wide tagline instead of the
event-specific name/photo, even though the bot UA was hitting our
already-existing OG handler. He compensated with a Cloudflare Worker as
SSR middleware — which then created bug 3 below (og:image at the
auth-gated /api/.../hero/ path, not the public /og/.../cover one), so
Instagram never rendered the image either.

## Bug A — slideshow URLs miss the OG handler entirely

`/gallery/<slug>/show/<token>` has 3 segments after `/gallery/`. The OG
route was wired only at `/gallery/:slug/:token?` (1-2 segments), so
slideshow links fell through to the SPA-catchall `/gallery/*` and never
invoked the OG handler at all. Added a second route handler for the
3-segment slideshow shape, sharing the same intercept middleware so a
recognised social crawler still gets the rich preview.

## Bug B — share-token-only URLs resolve to nothing

`/gallery/<32-char-share-token>` (the form produced when migration 525's
short-URLs option strips the event slug) routes to the OG handler with
`slug=<token>`. resolveSlug then queries `events.slug = <token>`, which
never matches because the token is in a separate `share_token` column.
Result: falls through to the "no event found" branch and serves the
generic site-wide OG.

Fix: when the slug shape matches a 32-char hex AND the slug lookup
missed AND no redirect rule applies, try `events.share_token = slug` as
a final fallback. Real slugs are kebab/dot/underscore mixes, never pure
32-hex, so the extra DB roundtrip is gated to only fire for the
token-shaped URL.

## Tests

3 new tests in galleryOgService.shareImage.test.js using non-entropy
32-hex fixtures (deliberately zero-padded to avoid tripping
GitGuardian's Generic High Entropy Secret detector while still
matching the route's /^[a-f0-9]{32}$/i shape check):
- share-token slug resolves via the share_token column (alex's case)
- malformed/expired 32-hex token returns the site-wide fallback (no leak)
- non-hex slugs skip the share_token query entirely (hot-path cost guarded)

All 14 tests in the file pass.

## Out of scope here (separate follow-up)

- Issue 2 (Instagram og:image) — alex-side CF Worker bug pointing
  og:image at /api/gallery/<slug>/hero/<id>, which requires gallery
  auth. PicPeak already has the right unauthenticated path
  (/og/gallery/<slug>/cover) gated by events.og_image_share_enabled
  per-event opt-in (#474). Documented in the issue reply.
- Issue 3 (URL shortener with custom names) — real feature request,
  meaningfully different from the existing #525 short-URLs option that
  just strips the slug. Designing separately.
2026-06-30 16:03:49 +02:00