First half of the GUI-only backup roundtrip. Adds a self-describing
".picpeak" archive that can be downloaded from one instance and (later)
re-uploaded to another via the web UI only.
- picpeakExportService.createPicpeak(): dumps every table as NDJSON
(tables introspected at runtime — no hardcoded list, won't rot), plus
a manifest (format version, app version, DB engine, latest migration,
per-table row counts + checksums, includePhotos, contains_secrets),
plus files/ (business-docs + uploads always; original gallery photos
only when includePhotos). NDJSON is engine-neutral so the target
rebuilds schema via migrations then loads rows — enabling pg↔pg /
sqlite↔sqlite and forward-only auto-migrate.
- GET /admin/backup/picpeak/export?includePhotos= streams the file and
sets X-Picpeak-Contains-Secrets (the file holds plaintext SMTP pass,
admin hashes, API keys — the UI must warn).
- Purely additive: no existing backup/restore path is touched.
Integration test proves the archive shape, knex-table exclusion, and
row-count/NDJSON consistency (85 tables on the seed schema).
Restructure picpeak-setup.sh around two clear modes:
- Interactive wizard (run_wizard): asks method → install dir → channel →
domain → HTTPS handling → admin email → SMTP, then shows a review and
confirms before installing. Each value already passed as a flag is
respected and its question skipped.
- Unattended (--unattended + flags): validate_unattended fills defaults and
fails fast on impossible combos (e.g. --enable-ssl without --domain).
New flags: --admin-password, --install-dir, --channel.
Align the Docker path with the rest of the project:
- Use the committed docker-compose.production.yml (prebuilt GHCR images) via
COMPOSE_FILE in .env instead of hand-generating a divergent compose file.
- Drop the broken setup_ssl_docker call (was referenced but never defined).
- Update path pulls images instead of building.
Admin bootstrap follows the browser-first model (#714): by default no
password is written; the one-time /setup token is surfaced (from
data/SETUP_TOKEN or the logs) with browser instructions. --admin-password
keeps the legacy seeded-admin + ADMIN_CREDENTIALS.txt flow for headless runs.
Depends on #714 (setup-token backend + secrets-init in production compose)
for the browser-first + zero-secret behavior at runtime.
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.
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).
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.
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.
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.
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.
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).
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.
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.
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.
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.
- 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.
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.
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.
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.
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
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 <slug>; React renders the literal
entity, so the banner read '/s/<slug>'. Decode the entities
(< > & " '), & 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.
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.
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.