Commit Graph

54 Commits

Author SHA1 Message Date
Paul Nothaft 25fbefc703 docs(faces): link the face-recognition guidance from where people look (#1125)
Every other feature routes readers to docs.picpeak.app. Face recognition was the
one that either pointed somewhere else or pointed at nothing — poor placement
for the feature with the highest read-before-you-enable burden anything here
ships.

.env.example referenced docs/feature-face-recognition.md, which does not exist —
and creating it is not the fix, because .gitignore:89 ignores docs/feature-*.md
outright, so the file would be invisible to anyone who cloned. That was the only
pointer to legal guidance an operator got while editing the variables that turn
Art. 9 processing on.

Also: the README linked the sidecar's developer README for the feature name and
had no row in the documentation table, docs/single-container.md left readers who
wanted the feature nowhere to go, ml/README.md had no backlink, and the admin
consent callout had no link at all. It does now, inline at the end of the
obligation.

Reported by @Luca-Timo.
2026-08-22 21:38:25 +02:00
Luca 7223118b89 feat(deploy): make the all-in-one image installable without a shell (#1124)
The all-in-one image could not be installed from a GUI at all — the deployment it
exists for. validateEnv treats a missing JWT_SECRET as critical and exits, and the
documented run command supplies it with `openssl rand`, a shell command a Synology
Container Manager or QNAP Container Station form cannot run.

wait-for-db.sh now generates one on first start and persists it next to the database,
extending the existing /run/secrets hydration rather than adding a second mechanism.
Explicit env still wins, then /run/secrets, then the generated file. The write is
load-bearing: JWT_SECRET is exported only when the file actually persisted, because an
unpersisted secret would mint a new one every restart and sign every session out.

Creation writes to a private temp file and hard-links it into place — atomic, fails with
EEXIST when another container won, and the loser adopts the winner's value. Non-regular
paths are rejected before the link, since POSIX ln links INTO a directory rather than
failing, which would make a mistyped -v target unrecoverable.

Also repairs the onboarding paths a new install actually walks: the installer no longer
rotates the secrets of a running install on re-run, deprecates the dead scripts/install.sh
in place, corrects the CONTRIBUTING dev loop, and fixes the vite proxy target that had
been pointing at a stray local port since 0da45e69.

Reviewed over three rounds. Co-authored by @Luca-Timo.
2026-08-22 18:37:12 +02:00
Luca e47c103c2a docs(readme): point the single-container install at a tag that exists
The all-in-one quickstart tells people to pull
`ghcr.io/picpeak/picpeak/aio:stable`, which has never been published, so
the documented one-liner fails for anyone who copies it:

    docker: Error response from daemon: failed to resolve reference
    "ghcr.io/picpeak/picpeak/aio:stable": not found

`merge-aio` does gate `:stable`/`:latest` on `refs/heads/stable` or a
non-prerelease `v*` tag, same as backend/frontend — but `Dockerfile.aio`
only landed on `main` in 0874a30a (#1068, 2026-08-18), and the current
`stable` head (3.46.1) does not contain it. So the aio image has only
ever built off `main`/beta refs, and its full tag list on GHCR and Docker
Hub is `main`, `beta`, and `3.10x.y-beta.0`. backend and frontend both
have `stable` and `latest`; aio is the only image that does not.

Point the quickstart at `:main`, which exists today, and note when
`:stable` will start working so this can flip back after the next stable
promotion.
2026-08-20 11:51:36 +02:00
Luca 899c9b3407 docs(docker): Hub pages for aio + ml, and the image table in the README
picpeak/ml has an empty Hub overview and picpeak/aio has none at all,
while backend and frontend carry hand-written ones — so the two newest
images are the two with nothing on their registry page.

Adds .github/dockerhub/{aio,ml}.md as the source of those pages and a
dockerhub-descriptions job that pushes them on every main merge, so the
page cannot drift from the release it describes. backend/frontend stay
hand-maintained for now: capturing their current Hub text into files is
a prerequisite, not a side effect of this change.

README gains a registry table for all four images (both registries share
digests and tags), the org-move callout lists the full set, and the
feature list finally mentions People in this gallery, which shipped in
#1074 without a README line.
2026-08-20 08:24:19 +02:00
Luca 0874a30ac9 feat(docker): all-in-one image (#1042) — my version of #1067 (#1068)
* feat(docker): add all-in-one image — backend + frontend in one container (#1042)

One container, one Node process, SQLite by default: `docker run` with no
compose file, no nginx, no supervisor, no bundled Postgres/Redis.

- Dockerfile.aio (repo-root context): frontend build stage + backend deps
  stage + a runtime stage mirroring backend/Dockerfile's production stage,
  with the built SPA copied to /app/frontend/dist and SERVE_FRONTEND=true.
  DATABASE_CLIENT=sqlite3 and STORAGE_PATH=/app/storage are pinned
  explicitly — the storage fallback resolves to container-root /storage,
  which EACCESes after the su-exec drop.
- server.js: the SERVE_FRONTEND block now does what the nginx image did —
  renders ${BRAND_TITLE}/${BRAND_DESCRIPTION} into index.html once at boot,
  serves that rendered shell on /index.html and every SPA route, caches
  hashed /assets/* immutably while the shell revalidates, and gzips the
  bundle via compression() mounted after all /api routers. express.static
  now runs with index:false so `/` keeps flowing to handlePublicSiteRequest
  — its default index option was shadowing the landing page on native
  installs.
- wait-for-db.sh: skip the Postgres readiness wait when DATABASE_CLIENT is
  sqlite3. The engine resolver still runs, still logs, and still refuses
  the populated-both conflict (#1038).
- .dockerignore: **/node_modules, so the root-context build can't pick up
  host deps from backend/ or frontend/.
- docker-build.yml: build-aio / merge-aio follow the same per-arch build →
  digest-merge → per-version tag scheme as backend/frontend (GHCR only for
  now; the Docker Hub mirror is wired once the Hub repo exists), plus a
  smoke-aio job that boots the image on every PR and asserts /health, the
  SPA shell, the rendered brand title, immutable asset caching and the
  SQLite engine resolution.

Pointing DB_HOST/DB_USER/DB_PASSWORD + DATABASE_CLIENT=pg at an external
Postgres works exactly like the backend image.

* fix(ci): correct three smoke-aio assertions that would fail a green image (#1042)

Found by running the smoke job locally against a real build — the image
passed every behavioral check, but three assertions were wrong:

- `/` asserts 200, but handlePublicSiteRequest 302s to /admin/login while
  the public landing site is disabled, which is the state of the fresh
  install the smoke container always is. Assert the redirect target
  instead — that still proves express.static's index option is not
  shadowing the handler, which is the thing the check exists for.
- The placeholder-leak grep matched index.html's explanatory comment,
  which mentions BRAND_TITLE in prose and survives into the built shell.
  Match the literal ${BRAND_TITLE}/${BRAND_DESCRIPTION} tokens with -F,
  and cover the description token too.
- Add a gzip assertion, probing with GET: the compression middleware
  skips bodyless responses, so a HEAD probe reports no Content-Encoding
  even when compression is active.

Verified locally on linux/arm64: image builds clean, boots to healthy in
~8s on the SQLite default, and 25/25 checks pass (SPA shell, rendered
brand title, immutable+gzipped assets, no-store shell, SPA fallbacks,
npm removed, su-exec drop to nodejs, no errors in the boot log). The
DATABASE_CLIENT=pg override was exercised against a real Postgres too —
the readiness wait still runs and the engine resolves to postgres.

* fix(server): serve the SPA for every client route, not just /admin and /gallery (#1042)

nginx did `try_files $uri $uri/ /index.html`, so behind compose every
client-side route survived a direct hit or a refresh and the short
`['/admin', '/admin/*', '/gallery/*']` list was never exercised. Without
nginx that list is the whole contract, and everything outside it 404'd:

  /setup  /customer  /impressum  /datenschutz  /payment-check
  /quote/:token  /contract/:token  /invite/:token
  /transfer/:token  /transfer-upload/:token

/setup is the first URL a new install visits, so the all-in-one image was
unusable from a cold start.

The catch-all is registered after `app.use('/api', notFoundHandler)`, so
an unknown /api route still answers JSON instead of being handed the HTML
shell, and after the /s/:shortSlug resolver, so a typo'd short URL still
404s (#699). It is GET-only — a stray POST keeps 404ing rather than
getting a 200 page back. The handler is hoisted out of the
SERVE_FRONTEND block via `spaCatchAll` because that block runs before the
API 404 handler is registered.

Verified on the built image: all ten routes above now 200, /api/nope still
returns JSON 404, /s/nonexistent still returns 404, / still 302s to
/admin/login, and the smoke suite is 25/25. Both boundaries are now
asserted in the smoke-aio job.

* docs(readme): document the single-container install (#1042)

The README had no mention of the all-in-one image, so the only way to
discover it was reading the workflow file. Adds a Quick Start subsection
with the one-line `docker run` and the `docker exec … cat SETUP_TOKEN`
step, plus a row in the documentation table.

Deliberately does not sell it as the default: the note says the compose
stack is still the right choice for anything busier, gives the reason
(SQLite takes one writer at a time), and points at the `.picpeak`
restore as the way out, so nobody picks it and then finds themselves
stuck. Full details live at docs.picpeak.app/deployment/single-container
(PicPeak/docs#8).

* feat(docker): fold #1067's items into the all-in-one image (#1042)

Consolidating the two parallel AIO branches into this one. This PR's approach
is kept wherever the two differed on design — in particular the in-process
brand render, `index: false` (which fixes express.static shadowing
handlePublicSiteRequest, a bug #1067 had), the compression middleware, and the
smoke-aio job. What follows is what #1067 had that this branch did not.

Layout — the issue asks for a single mountable root, and this moves to one:

  /data/db       picpeak.db (+ -wal/-shm) and SETUP_TOKEN
  /data/storage  originals, thumbnails, archives
  /data/logs     application logs
  /data/backup   built-in backup output; /backup symlinks here

`-v picpeak:/data` and nothing else to remember. README and the smoke job's
database-path assertion follow the new layout.

Correctness items:

- sqlite CLI. DatabaseBackupService SPAWNS `sqlite3` for `.backup` and
  PRAGMA integrity_check; the npm module does not ship that binary.
  backend/Dockerfile omits it because compose always runs Postgres — this
  image defaults to SQLite, so every database backup failed with ENOENT.
- /backup wired in. Migrations 029 + 030 seed /backup/picpeak and
  /backup/database as the backup destinations; nothing created or mounted them,
  so backups had nowhere to write and anything written would die with the
  container. Symlinked into the volume, subdirectories created at startup
  (a bind mount hides the tree baked into the image), and adopted only when
  BACKUP_DIR is set so it never gates boot for compose deployments that do not
  mount it.
- logger.js honours LOG_DIR. It hard-coded <backend>/logs, so logs could not
  leave the container. Unset keeps the old path for every existing install.
- wait-for-db.sh derives its writable roots from STORAGE_PATH / DATA_DIR /
  LOG_DIR instead of hard-coded /app paths, and mkdir -p's them before chown —
  a bind-mounted /data hides the image's tree, and chown against a missing path
  reports "the filesystem rejects chown", which is both wrong and a dead end.
- .dockerignore excludes backend/-prefixed runtime data. Docker reads only the
  root file, so the unprefixed data/*.db, logs/* and storage/* rules missed
  backend/data, backend/logs and backend/storage entirely; a checkout used to
  run PicPeak would bake its database, photos, logs and SETUP_TOKEN into a
  published layer.
- HEALTHCHECK follows $PORT rather than a hard-coded 3000.
- --max-http-header-size=32768 matches nginx's large_client_header_buffers
  4 32k; Node's 16 KiB default would reject a guest carrying several
  per-gallery JWT cookies.

docs/single-container.md is added as the in-repo reference the README links to.

The smoke job gains four assertions for the above: the one-volume layout and
writable backup destinations, the sqlite3 CLI, logs landing on the volume, and
the image carrying no runtime data from the build context.

Verified on a built image — named volume, bind mount and PORT=8080 all healthy;
every existing smoke assertion still passes, including / -> 302 /admin/login,
the rendered BRAND_TITLE, immutable assets, gzip and /s/<unknown> -> 404.

Co-authored-by: Luca-Timo <102960244+Luca-Timo@users.noreply.github.com>

* fix(docker): restore the SPA-fallback exclusions and close the build-context leak (#1042)

Both found by external review of the consolidated branch.

- The SPA catch-all had no backend-owned exclusions. This was a regression I
  introduced while merging: #1067 carried a BACKEND_OWNED prefix list, and
  taking this branch's server.js wholesale (correctly — its index:false and
  in-process brand render are the better design) dropped it. /photos,
  /thumbnails, /uploads and /fonts are static mounts whose middleware calls
  next() on a miss, so the catch-all was answering 200 text/html under image
  and font URLs instead of 404. nginx gave each of those its own location
  block, so try_files never applied to them.

- backend/data is now excluded wholesale rather than by suffix. The suffix list
  (*.db, *.db-wal, *.db-shm, SETUP_TOKEN) let real secrets through: a used
  checkout carries ADMIN_CREDENTIALS.txt next to the database, plus -journal
  files and any DATABASE_PATH not ending in .db. Since Dockerfile.aio builds
  from the repository root and COPYs backend/ wholesale, any of those would be
  baked into a published layer. The directory holds only runtime state and is
  already gitignored in full.

smoke-aio gains an assertion that the backend static routes still 404, so the
exclusion cannot be dropped again silently.

Verified on a built image: /photos, /thumbnails, /fonts and /uploads misses all
404; /setup, /impressum, /gallery/x, /admin/login still 200; / still 302s to
/admin/login; /api/nope still answers JSON; /s/<unknown> still 404s; and the
image carries no *.db, ADMIN_CREDENTIALS.txt, logs or storage from the context.

* fix(aio): three failures that only surface outside a dev laptop (#1042)

Backups aborted on SQLite. getTableChecksums() built its digest with
`CAST(t.* AS TEXT)`, which is Postgres row-to-text syntax; SQLite parses
`*` there as a syntax error, so every backup threw before reaching the
.backup call. Since the all-in-one image ships SQLite by default, that is
every AIO install. Enumerate the columns via columnInfo() and sum their
lengths instead.

The shared /data mount root was never adopted. wait-for-db.sh chowned the
children it creates but not the mount point itself, so a host directory
arriving as 0700 with a foreign owner stayed untraversable by UID 1001
after the su-exec drop. Docker Desktop's permissive bind mounts hide this
completely, which is why local testing passed; a NAS share does not.
DATA_ROOT is now adopted first.

Maintenance mode locked the admin out of the box. The middleware runs at
server.js:493, long before the static block at 891, and exempted the auth
endpoints but not the page that calls them. With the backend serving the
frontend, /admin/login and /assets/* returned 503 JSON, so an admin who
enabled maintenance mode could never load the UI to turn it off. nginx
serves those paths in the compose stack, which is why it never surfaced
there. Guest and API surfaces stay gated.

Verified on a built image: checksums compute across all 95 tables; a bind
mount created 0700/4000:4000 boots healthy and ends up 1001:1001; with
general_maintenance_mode=true, /admin/login, /admin and /assets/* return
200 while /gallery/* and /api/gallery/* return 503 — and 503 across all
three once the exemption is removed again.

Claude-Session: https://claude.ai/code/session_01Ra4hcsYiKuQLbbRsg6EjAc

* fix(aio): stop leaking .env into the image, fix the broken checksum test (#1042)

The Jest suite was red: mocking db.raw is no longer enough now that the
SQLite checksum branch asks the query builder for its column list, so
db(table) came back undefined and getTableChecksums failed on every PR.
The production code is right; the fixture needed to know about the call.

backend/.env was landing in the published layer. The root ignore file's
`.env`, `.env.*` and `data/*.db` rules read as unanchored but Docker
matches them from the context root, so they catch ./.env and never
backend/.env — and `COPY backend/ .` then puts a real JWT_SECRET at
/app/.env. Matched at any depth instead, the way **/node_modules in the
same file already is. Confirmed by building from a checkout carrying a
planted secret: before, `cat /app/.env` printed it back.

Business documents wrote outside the volume. quoteService, invoice
sending/reminders and contract signatures build paths from
process.cwd()/storage and never read STORAGE_PATH; compose hides it by
setting STORAGE_PATH=/app/storage with WORKDIR /app so the two are the
same directory. Here they are not, and /app is root-owned, so a quote or
invoice PDF failed to write as UID 1001 — and would not survive the
container if it had. Symlinked /app/storage into the volume, matching
the /backup symlink beside it. Teaching those services STORAGE_PATH is
the real fix and wants its own change.

Two smaller ones: the mount root is now chowned shallow rather than
recursively, since every child below it is already walked recursively
and a NAS-sized photo library should not be traversed twice on each
restart; and /assets/ joins the backend-owned prefixes, so a stale
hashed chunk requested by a tab left open across an upgrade gets a 404
instead of index.html served with 200 under a .js URL.

Verified on a built image: planted backend/.env and backend/probe.db are
absent; /app/storage resolves to /data/storage and a business-doc write
as UID 1001 appears on the host; a 0700 bind mount owned by 4000:4000
boots healthy; a missing /assets chunk 404s while the real bundle still
serves 200 as application/javascript. The databaseBackup suite is green
again, and the branch adds no failing suite that origin/main does not
already fail on the same machine.

Claude-Session: https://claude.ai/code/session_01Ra4hcsYiKuQLbbRsg6EjAc

* test(aio): teach the leak assertion about the storage symlink (#1042)

The previous check listed /app/storage/events and treated a hit as a
leak. That was true while /app/storage was either absent or a copied
directory; now it is a symlink into the volume, so the check followed it
and found the empty tree the image itself creates — a false positive on
its own design.

Check the shape instead: /app/storage must be a symlink pointing at
/data/storage, and the volume's photo tree must contain no files on a
fresh install. A real directory there now fails loudly, which is the
condition the assertion was always trying to catch. Also extended the
path list to /app/.env and loose database files, matching the
.dockerignore rules added alongside.

Claude-Session: https://claude.ai/code/session_01Ra4hcsYiKuQLbbRsg6EjAc

* fix(aio): show the maintenance screen instead of raw JSON to guests (#1042)

The previous commit exempted the admin shell so an admin could still
reach the switch they had just flipped. Guests had the same problem for
the same reason: with no nginx in front, /gallery/<slug> reaches this
middleware long before the static block, so a visitor during maintenance
got a 503 JSON body where every other deployment shows the branded
maintenance screen the frontend already ships.

Replaced the two path-specific exemptions with the rule they were both
special cases of: a GET that is not an API call and not a backend-owned
content mount is the SPA shell, and the shell is inert HTML — it boots,
reads /api/public/settings (already exempt) and renders MaintenanceMode
on its own. Everything that carries real data stays gated: /api/*,
/photos/, /thumbnails/, /fonts/, and any non-GET.

Compose is untouched by construction, since nginx answers those paths
and they never arrive here.

Verified on a built image with the flag on: /gallery/x, /customer/x,
/admin and /admin/login return 200 text/html while /api/gallery/x/verify,
/photos/x.jpg and /thumbnails/x.jpg return 503 and a POST to a public API
still returns 503; with the flag off the same paths go back to 404. Added
a middleware test over that exemption matrix — over-exemption is the real
risk in this change, so it asserts the gated half too. It fails on five
cases without the fix.

Claude-Session: https://claude.ai/code/session_01Ra4hcsYiKuQLbbRsg6EjAc

* fix(aio): stop the shell exemption from un-gating /og and the public CMS (#1042)

The previous commit exempted "any GET that is not an API call". That
negative rule reads as safe and is not: /og/gallery/<slug> and its
/cover render the event name and the hero thumbnail, /s/<code> renders
short-link previews, and `/` is handed to the public CMS. All four are
proxy_passed to the backend by nginx, so they were gated before this PR
in every deployment — the rule un-gated them, and for compose too, not
just the new image. A site switched to maintenance would have kept
publishing gallery metadata.

Replaced the guess with the split nginx already defines: exempt what the
frontend container answers itself, gate what it proxies. That is the
same rule the all-in-one image needs by definition, since its whole job
is to be both halves of that stack, and it now matches compose in both
directions rather than only in the direction the last commit tested.

Verified on a built image with the flag on: /admin/login,
/gallery/<slug> and /customer/* return 200, while /, /og/gallery/x,
/og/gallery/x/cover, /s/abc, /robots.txt, /api/* and /photos/* return
503; with the flag off all of them behave normally again. The middleware
test grew the gated cases — it now covers 21, most of them asserting
what must NOT be exempt.

Claude-Session: https://claude.ai/code/session_01Ra4hcsYiKuQLbbRsg6EjAc

* fix(aio): give the image a FRONTEND_URL default so share links are absolute (#1042)

getFrontendBaseUrl() reads FRONTEND_URL, falls back to the
general_site_url setting, and otherwise returns an empty string — which
makes share_url come back as a bare "/gallery/<slug>/<token>". Compose
defaults the variable to http://localhost:3000, but the documented
one-liner for this image passes only JWT_SECRET, so every fresh
single-container install handed out relative links in API responses, QR
codes and emails.

Defaulted to the same value compose uses; -e FRONTEND_URL=https://...
overrides it, as does the site URL field in Settings.

Found by pointing tests/e2e/local at a running AIO container:
auth/06-api-tokens asserts share_url matches /^https?:\/\//, and it was
the one spec that failed for a product reason rather than a harness one.
It passes now, and the suite is 19/20 against the image — the remaining
failure is smoke/02-auth-flow, whose seed helper shells out to a
hard-coded `docker exec picpeak-backend`, so it cannot arrange its
precondition against any other container.

Claude-Session: https://claude.ai/code/session_01Ra4hcsYiKuQLbbRsg6EjAc

* feat(aio): mark the image so face recognition stays off (#1042, #1074)

Face recognition needs a separate ML container this image does not contain,
and enabling it here would add a second image-processing pipeline competing
with Sharp for the CPU and memory of a container sized for one photographer
plus guests browsing. The failure mode would not be a clear error — just a
slow install that looks broken.

The backend gate for this lands in #1075 and keys on PICPEAK_SINGLE_CONTAINER.
Without this line the guard never triggers on an actual all-in-one build, so
the two changes have to arrive together: whichever merges second completes
the pair. Verified against this file's exact value — isFeatureEnabled()
returns false with it set.

An explicit marker rather than inferring from SERVE_FRONTEND or the SQLite
path, because legitimate multi-container deployments do both of those and
should keep the feature.

Also adds it to the Limits section of docs/single-container.md, next to the
SQLite and Redis constraints, since that is where someone will look before
choosing this image.

---------

Co-authored-by: Paul Nothaft <paul@MacStudio-von-Paul.local>
Co-authored-by: the-luap <paul-nothaft@hotmail.de>
2026-08-18 22:15:49 +02:00
Luca 27dedb13f3 docs: flip README links to docs.picpeak.app + delete docs/_to-migrate (#1000 phase 3) (#1023)
Phase 3 (final) of #1000. The deep content now lives on the docs site (PicPeak/docs#7), making docs.picpeak.app the single source of truth and removing the in-repo copies.

README links flip to docs.picpeak.app; the roadmap table is retired in favour of GitHub Issues. Deletes docs/_to-migrate/ and the five migrated pages. docs/migration-to-org.md stays — it's repo-transitional, not docs-site content.

In-app references to the deleted files are repointed at the docs site, including the CRM disclaimer strings in en.json/de.json and the contract-editor fallback.

Closes #1000.
2026-08-11 10:38:35 +02:00
Luca ddebd50d3f docs: slim README to a lean router, stage deep content for docs-site migration (#1001)
Phase 1 of the README slim / docs-migration plan in #1000.

README goes from 577 to ~191 lines: hero, one Quick Start, a Documentation
index, comparison table, tech stack and a table of contents. The deep inline
prose moves into a temporary docs/_to-migrate/ staging folder (webhooks,
storage backends, first-run setup, system requirements, roadmap) so README
links keep resolving until the docs-site pages are live.

Existing docs/*.md referenced by app code are deliberately left in place —
crm-disclaimers.md (frontend TSX, i18n, a backend route and migration),
fonts.md (server.js), accounting-inbound-invoices.md (Dockerfile) and
migration-to-org.md (UpdateNotification.tsx, MigrationBanner.tsx). Moving them
is a separate, code-touching change.

Verified before merge: merges cleanly against main with no conflicts; all 14
in-repo links resolve in the merged tree; no docs file is deleted or renamed;
and the registry-move notice from #995 survives the rewrite in condensed form,
keeping 'still responds but its tags are frozen at 2026-05-27' plus the
migration-to-org.md link. The fuller symptom explanation remains in that doc,
which the README links to.

Follow-up per #1000: port docs/_to-migrate/* into docs.picpeak.app, then flip
the README links and delete the staging folder.

Co-authored-by: Luca-Timo <Luca-Timo@users.noreply.github.com>
2026-08-10 08:16:13 +02:00
Paul Nothaft b9e42591f5 docs: the retired registry path freezes, it does not stop serving (#995)
Closes #985.

README and migration-to-org.md both claimed the old path 'is no longer served'.
It is served — ghcr.io/the-luap/picpeak/backend:latest returns a complete image,
created 2026-05-27, label version: main. The registry responds normally; it just
never receives anything new.

That inaccuracy is what generates reports like #982. Told the path is not
served, an operator runs docker compose pull, watches it succeed, runs docker
rmi and pulls again, watches that succeed too, and concludes the problem lies
somewhere other than their image path. Nothing reports an error anywhere; the
only symptom is an update notice that never resolves.

Say what actually happens — the path freezes rather than failing — and add a
self-diagnosis via docker image inspect on both paths, with the 2026-05-27 date
and the 'main' version label as the tells. MigrationBanner's wording is left
alone: 'no longer being updated' was accurate.

This is the delivery mechanism for #985. There is no in-app channel:
MigrationBanner shipped a month after the freeze, the #993 update-check notice
cannot fire on installs running their own frozen backend, and the changelog
modal that renders release notes shipped two days after the freeze. What reaches
these operators is GitHub, and the GHCR page for the retired package — which
renders this README through the images' own org.opencontainers.image.source
label, so the fix propagates to the dead path's own page automatically.
2026-08-04 17:30:18 +02:00
Paul Nothaft 1b4e5fee3e fix(security): bound inbound-mail resources, redact secrets from logs (GHSA-2qf9, pgmp, r794) (#959)
* fix(security): bound inbound-mail resources, redact secrets from logs (GHSA-2qf9, pgmp, r794)

GHSA-2qf9 — emailIntakeService downloaded, parsed and persisted every message
with no size, attachment-count or attachment-byte limit, reachable
unauthenticated by anyone who can email the operator's mailbox:
- fetch the envelope with `size` (same cheap pass) and refuse an oversized
  message BEFORE downloading its source;
- cap attachment count and cumulative attachment bytes;
- limits env-overridable, defaults generous for real supplier invoices.

The teeth were in the dedup key. received_emails.message_id is varchar(512)
UNIQUE, and the failure path wrote `err-<uid>-<Date.now()>`, which can never
match the envelope-derived messageId the dedup pass compares against — so an
oversized (or overlong-Message-ID) mail was re-downloaded every poll forever,
and an OOM-kill/restart just resumed the loop. Size-skips are now recorded
under the REAL message id, and overlong ids collapse to a stable sha256 key
that always fits the column.

GHSA-pgmp / r794 — new sanitizeForLog() util (key-name deny-set, recursive,
cycle-safe) applied to the three request-body log sites in adminEvents/crud.js,
plus sanitizeValidationErrors() because express-validator's errors.array()
embeds the SUBMITTED value per field — a rejected plaintext password was still
logged. Scope is wider than filed: the update path also logged
client_password_hash and a LIVE client_share_token bearer credential.

Also: the one-time setup token was logged at warn AND printed to stdout on
every first boot, putting a live first-admin credential in combined.log,
security.log and `docker logs`. It is now written to the 0600 token file and
only surfaced when that write fails — the last-resort path it existed for.

* fix(security): codex round 3 — repair the first-run token recovery flow (GHSA-r794)

Two regressions from keeping the setup token out of the logs.

1. server.js decided whether to print the token by calling existsSync() on the
   candidate path. That answers a different question than "did the write
   succeed": a stale, read-only or directory-shaped SETUP_TOKEN reports as
   present, so the banner suppressed the live token and pointed the operator at
   content that is not it — leaving the current token only in combined.log
   under default production logging. setupService now records the path the
   write actually produced and exposes it via writtenSetupTokenFile().

2. The setup screen, its EN/DE strings, README, SIMPLE_SETUP and .env.example
   all still told first-time users to run
   `docker compose logs backend | grep -i "setup token"`. On the normal path
   that command now returns a path banner and no credential, so the documented
   browser-first onboarding could not be completed. They now point at
   `docker compose exec backend cat /app/data/SETUP_TOKEN`, with the log
   fallback described as what it is — the failure path.

Claude-Session: https://claude.ai/code/session_01F211U4dDbEj4zXiyKbi9me

---------

Co-authored-by: Paul Nothaft <paul@MacStudio-von-Paul.local>
2026-08-02 21:17:11 +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
Luca de789faec5 Merge pull request #698 from Luca-Timo/docs/comparison-pixieset 2026-06-30 12:35:29 +02:00
Luca b0439638bd docs(readme): clarify comparison footnotes — $0 cost caveat + Pixieset video cap
- you still bring your own server (own hardware or VPS) and optional domain.
- Pixieset "unlimited" storage is photos only — video is capped per plan (~0–10 h depending on tier).
- Renumber the PicPeak storage footnote (* → **) so the three markers don't collide.
2026-06-30 11:20:37 +02:00
Luca 721f440fa6 docs(readme): add Pixieset to comparison + customer-accounts/CRM/accounting rows 2026-06-30 11:07:58 +02:00
Luca 748238e8ca docs(readme): credit @the-luap as creator/lead maintainer 2026-06-30 11:06:04 +02:00
Paul Nothaft 5839bba72a docs: prominent migration banner at the top of README (#669)
GitHub-flavored `> [!IMPORTANT]` callout right below the title, before
the badges/hero block, so it's the first thing a visitor or repo browser
sees in the rendered README. Mirrors the in-app banner (#687) so an
operator gets the same message whether they're browsing the repo or
logged into the admin dashboard.

Body covers:
- Image-path change with the literal new path
- Branch rename (beta → main, main → stable) with auto-redirect note
- Link to docs/migration-to-org.md for the exact compose-file edit

Remove (or downgrade to a regular note) after the migration window
settles, same lifecycle as the in-app banner constant.
2026-06-29 22:24:56 +02:00
Paul Nothaft 0205c7dcce chore: migrate Docker registry + GitHub URLs to PicPeak org
Repo transferred from the-luap/picpeak → PicPeak/picpeak. Docker images
publish to ghcr.io/picpeak/picpeak/{backend,frontend} (lowercase, per the
GHCR canonical form computed by docker-build.yml's `${GITHUB_REPOSITORY,,}`).

Sweep covers:
- docker-compose.production.yml + Dockerfiles → new image registry path
- README, CONTRIBUTING, SECURITY, SIMPLE_SETUP, scripts/picpeak-setup.sh
  → new GitHub URLs
- Update-check / release-notes services (updateCheckService,
  environmentService, updateNotificationService, adminSystem,
  UpdateNotification, githubReleaseUrl) → GitHub API + tag URLs use the
  canonical PicPeak/picpeak path
- Issue templates + README-DOCKER + workflow README → updated package URLs
- One commit-context comment in migrations/090 + customerAccountsService

CHANGELOG.md is intentionally untouched (historical release entries are
immutable; GitHub auto-redirects the old URLs indefinitely).
CLAUDE.md keeps the bare `(the-luap)` reference — that's the maintainer's
personal handle, not a repo URL.

22 files, 48/48 line swaps (every change is a 1:1 URL replacement).
2026-06-29 20:20:13 +02:00
Luca 16013d1cf9 docs(slideshow): add Live Slideshow guide + README entries
- docs/live-slideshow.md: full feature guide (enable, generate link, run on a
  projector, global Settings -> Slideshow defaults, per-event overrides, how
  live updates work, security notes).
- README: Live Slideshow bullet under Key Features, a Live Events use case, and
  a Documentation quick link.
2026-06-20 13:30:44 +02:00
Paul Nothaft 714a9f6fb1 fix(upload): auto-throttle on low-memory hosts + correct documented RAM minimum (#628)
The README claimed 2GB RAM as the minimum, but two background-processor
worker loops × sharp.concurrency(2) means up to four libvips threads can
decode full-resolution images in parallel — peak RSS lands at 1.5GB+ on
a batch of 20MP+ photos. Add Postgres + Redis + Node baseline and one
heavy batch on a 2GB VPS OOM-kills the backend, surfacing as 503s on
thumbnails until restart:unless-stopped brings it back. Reported in #602,
filed as #628.

Three changes, smallest-surface-area each:

1. backgroundProcessor.js — on startup, when UPLOAD_PROCESSOR_CONCURRENCY
   is NOT set and os.totalmem() reports < 3GB, default to 1 instead of 2
   and log a one-shot warning naming the override env var. Explicit env-var
   setters keep their value. os.totalmem() reports container memory under
   cgroup v2 so this works in Docker / k8s as well as bare metal.

2. README.md — bumped the documented minimum from 2GB to 4GB, kept 2GB
   only as a "Low-memory hosts" recipe pointing at UPLOAD_PROCESSOR_CONCURRENCY=1
   with the throughput trade-off spelled out. Added the 503-on-OOM symptom
   so the next reporter finds it via search.

3. docker-compose.production.yml — commented mem_limit / memswap_limit
   example on the backend service. Off by default (don't surprise existing
   deployments) but visible to operators thinking about shared/multi-tenant
   hosts. restart:unless-stopped already on every service.

No code path for memory-aware runtime throttling (Luca's option 4) — out
of scope for a bug fix; tracked separately if #1-#3 don't close the case.
2026-06-17 23:04:30 +02:00
Luca 116743ba43 docs(readme): add CRM + accounting to features, tax disclaimer, update contributor
- New "For Studios — CRM & Accounting (Beta)" subsection under Key Features
  (quotes→contracts→invoices+Storno, hours/calendar, inbound supplier invoices +
  expenses, tax report + Treuhänder/Banana export, VAT) and updated the Roadmap
  beta-table row to "CRM & Accounting Module".
- Broadened the disclaimers section to CRM & Accounting and added a Tax/VAT
  bullet: figures are guidance only + jurisdiction-specific (e.g. the LI 20%
  Gewinnungskosten flat rate), and every operator must verify their own tax/VAT
  regulations with their accountant/Treuhänder/tax authority before relying on
  any figure or export.
- Updated the @Luca-Timo contributor entry with a concise CRM + accounting credit.
2026-06-16 19:05:38 +02:00
Paul Nothaft 9d424d0dbb Merge pull request #596 from Luca-Timo/bugfix/crm-backup
Backup & Restore hardening — close the silent files-only data-loss class
2026-06-02 09:10:57 +02:00
Luca 43cb0ea4bf docs: consolidate disaster-recovery into Backup & Restore guide
The previous split (separate docs/install-from-backup.md + separate
README link for "Disaster Recovery") fragmented what's conceptually
one workflow: backup → restore. DR is a specific scenario of restore
(the destination is wiped), not a separate feature.

This merge:
  - Folds install-from-backup content into docs/backup-restore.md
    as a "Disaster recovery (install from a backup)" section with
    its own table-of-contents anchor.
  - Adds an explicit ToC at the top so admins land on what they
    need in one click.
  - Frames the two restore paths up front: "live install" → wizard,
    "fresh / wiped install" → trigger file. Admins encountering DR
    in panic mode don't need to know to look under a separate link.
  - Drops the duplicate "Disaster Recovery" README bullet. The
    "Backup & Restore" blurb now mentions DR explicitly so it's
    still findable via Ctrl+F on the README.
  - Removes docs/install-from-backup.md (its content is now in
    backup-restore.md's DR section).

Single source of truth = less risk of one doc going stale relative
to the other when the feature evolves. Maintainer-facing surface
on docs.picpeak.app shrinks back to one /guides/backup-restore page.
2026-06-01 00:21:39 +02:00
Paul Nothaft c2dcd9ca84 docs(readme): list CRM module under Beta Features with own-risk disclaimer
PR #555 shipped the CRM module on beta. The README's "Beta Features
(Use at your own risk)" table is the right place to signal that the
feature exists, is opt-in, and carries non-trivial legal / financial
caveats — readers landing on the README should not first discover the
CRM by enabling its feature flags and bumping into the seeded
example contract bodies without warning.

Adds one row to the Beta Features table linking to
docs.picpeak.app/features/crm where the full disclaimers,
sub-feature pages, and admin-settings reference live.

CRM is intentionally NOT added to the top-of-README "Key Features"
list — those are stable, production-ready features. Mixing the beta
CRM in there would undermine the clear stable/beta distinction.
2026-05-29 16:39:11 +02:00
Paul Nothaft e537923857 Merge pull request #576 from the-luap/docs/release-cadence-565
docs(release): establish stable-channel cadence + promotion process
2026-05-29 16:24:59 +02:00
Paul Nothaft ab81998996 docs(release): establish stable-channel cadence + promotion process (#565)
Closes #565.

Beta has been the de-facto stable channel because the actual stable
lagged so far behind that new users following the README ended up
worse off than users who knew to switch to beta. The fix has two
parts: regular stable cuts (the PR #568 promotion is the first one)
and a written process so future cuts don't depend on memory.

This adds:

- RELEASING.md at the repo root — full operational doc with cadence
  target (4–6 weeks), promotion criteria (CI green + 7-day bug soak
  + upgrade-walk on real-shaped data + operator smoke), the actual
  beta→main mechanics including the conflict-resolution checklist we
  used in PR #568, hotfix backport path (with PR #412 as the worked
  example), and the project's versioning rules.
- CONTRIBUTING.md — replaces the four-line "Release Process" stub
  (which was wrong; it described a hand-rolled flow that release-please
  has handled for the last several releases) with a brief summary and
  a pointer to RELEASING.md.
- README.md — one-sentence addition to the existing "Release Channels"
  section pointing curious users at RELEASING.md.

No code change. CHANGELOG.md and version files are intentionally
untouched — release-please will catch this on the next regular cut.
2026-05-29 11:18:45 +02:00
Luca e7db0bb866 docs(crm): legal/financial disclaimers — examples only
Adds a top-level disclaimer section to README + a dedicated
docs/crm-disclaimers.md spelling out two areas where picpeak ships
defaults the operator MUST review before going live:

1. Contract blocks (image rights, NDA, model release, cancellation,
   jurisdiction, …) — written by the maintainer, NOT by a lawyer.
   Every operator must have their lawyer review and adapt them
   before sending any contract to a customer.

2. QR-bills and SEPA EPC payloads — rendered from the data the
   operator typed. Picpeak is open source; we recommend scanning a
   test invoice with the operator's bank app to verify the QR
   actually works.

Matches the on-screen amber disclaimers already shown on the
Contract Block Library page and the Business Profile payment-block
editor.
2026-05-26 18:20:55 +02:00
Paul Nothaft 1505775678 fix(install): self-chowning entrypoint kills fresh-install restart loop (#484)
The fresh-install restart loop reported by @MrGabri (and confirmed by
@AloePacci with the user:0:0 workaround) had a clear root cause:

  - Dockerfile pinned USER nodejs (UID 1001) before the entrypoint
    ran, so the existing chown branch in init-production.sh:13 was
    dead code.
  - wait-for-db.sh (the actual entrypoint, not init-production.sh)
    silently swallowed mkdir/EACCES on bind mounts with || true,
    then a downstream migration error surfaced as the visible failure.
  - Net effect on a typical Linux host where the bind-mount dir is
    owned by UID 1000: container can't write, exits non-zero,
    restarts forever with no clear error.

Switch to the standard Docker drop-privileges pattern:

  1. Install su-exec, drop `USER nodejs` from the Dockerfile —
     container now starts as root.
  2. wait-for-db.sh: if running as root, chown /app/storage,
     /app/data, /app/logs to nodejs and re-exec self via
     su-exec nodejs:nodejs. App still ends up running as UID 1001.
  3. Preflight check for non-root invocations (compose `user:`
     overrides): verify the bind mounts are actually writable
     before continuing. If not, exit 1 immediately with an
     actionable error pointing at the docs — no more silent
     restart loops.

Also:

  - Delete backend/init-production.sh. It was an orphan — no caller
    in the Dockerfile, compose, or anywhere else. Its chown logic
    looked authoritative enough that @MrGabri ran it manually trying
    to debug, which is what finally surfaced the EACCES.
  - docker-compose.yml: drop user: + PUID/PGID env. The pattern-B
    UID-matching workaround they implemented is obsolete now that
    pattern A (root-then-drop) is in place.
  - .env.example + README: drop PUID/PGID documentation.
  - Add fresh-install smoke test workflow. Boots backend + postgres
    against bind mounts owned by UID 1000 (the GitHub runner UID,
    and the common-mismatch case on Linux hosts) and verifies:
    + container reaches healthy without restart-looping
    + chown happened (dirs now owned by 1001 inside the container)
    + node runs as nodejs, not root (su-exec drop worked)
    + /health returns status:ok
    + with --user 5005:5005 + unwritable mounts, preflight exits
      loud with the expected error string

Verified locally end-to-end against a fresh Postgres + UID-501-owned
bind mount: backend reaches healthy in ~20s, chown applied, node
runs as nodejs, no restart loop. Docs in picpeak-docs cover the new
behavior + a Troubleshooting section for the install-path bugs
fixed in #484/#494/#511/#488.

Refs: #484
2026-05-17 22:29:29 +02:00
Paul Nothaft dbe0a3055b docs(readme): add Contributors section with @Luca-Timo and @Rekoo-PS
The Acknowledgments block had a generic "thanks to all contributors"
line but no actual recognition by name. Two people in particular have
moved the project meaningfully forward and should be called out:

- @Luca-Timo — code contributor across multi-arch Docker, the external-
  URL CMS toggle, folder tree picker, admin email picker, self-hosted
  webfonts, the gallery header/banner decoupling, and typed-API
  refactors. Consistent quality.

- @Rekoo-PS — bug reporter and feedback loop. Filed the issues that
  drove the login-loop fix, gallery loading skeleton, redirection
  cleanup, mobile lightbox overhaul, admin events search-counter fix,
  photo-count column, and bulk-delete workflow. Also a BuyMeACoffee
  supporter.

Closes the implicit recognition gap and sets up the section so future
contributors can be added with a one-line PR.
2026-05-04 21:35:27 +02:00
Paul Nothaft df3061893d Merge pull request #349 from Luca-Timo/feat/apple-silicon-support
feat: native multi-arch Docker images (Apple Silicon, ARM64 Linux)
2026-05-02 23:01:54 +02:00
Luca c7ac9ddfe5 refactor: rename mac override to amd64 override for arch accuracy 2026-05-02 01:34:46 +02:00
Luca c282a72bd3 feat: support Apple Silicon natively via multi-arch images 2026-05-02 01:26:47 +02:00
Paul Nothaft 0faf9b3281 docs: move documentation to docs.picpeak.app, drop in-repo copies
The full documentation now lives at https://docs.picpeak.app — built
from the picpeak-docs Nextra repo. The README, SIMPLE_SETUP, and the
v1 OpenAPI generation flow all point there now.

Removed (now living at docs.picpeak.app):
- DEPLOYMENT_GUIDE.md (root-level — content covered by docs.picpeak.app/deployment)
- docs/ADMIN_SETUP_GUIDE.md
- docs/JWT_SECRET_MIGRATION.md
- docs/SECURITY_BEST_PRACTICES.md
- docs/admin-api-quickstart.md → docs.picpeak.app/api
- docs/nginx-fix.md → docs.picpeak.app/deployment/reverse-proxy
- docs/openapi.json, docs/openapi.yaml → still generated locally as a
  build artifact (now gitignored), synced into picpeak-docs by
  scripts/sync-api-docs.sh
- docs/picpeak-admin-api.openapi.yaml → ditto

Kept:
- docs/*.png (logo + screenshots — README still img-tags these)

Updated:
- README.md — replaced six in-repo doc links with docs.picpeak.app
  pointers, restructured the Documentation section as a curated link
  list to the new site
- SIMPLE_SETUP.md — single deployment-guide link redirected
- .gitignore — docs/openapi.{json,yaml} are now build artifacts, not
  tracked
- backend/src/routes/v1/events.js — comment clarifies the OpenAPI flow
2026-04-29 22:24:15 +02:00
Paul Nothaft 1b717ce5ed feat: native S3 storage backend (#328) + presigned download follow-up
Lets PicPeak write photos, thumbnails, hero images, watermarks, and
archive zips to any S3-compatible bucket (AWS S3, MinIO, Cloudflare R2,
Backblaze B2, Wasabi, DigitalOcean Spaces) instead of the local
filesystem. Selected via STORAGE_BACKEND=local|s3.

Architecture
- backend/src/services/storage/StorageBackend.js — abstract interface
  (put/get/exists/stat/delete/list/copy/rename/signedUrl/putFromFile/
  getToFile) — typedef-only, documents the contract.
- LocalFsStorage.js — wraps fs with atomic-write-via-tmp-rename, path
  traversal protection, list-as-walker.
- S3StorageBackend.js — thin wrapper around the existing
  S3StorageAdapter (used by backupService) mapping it onto the canonical
  interface; supports optional STORAGE_S3_PREFIX namespace.
- index.js — factory selected by STORAGE_BACKEND with startup ping
  (HEADs sentinel key on S3, fs.stat on local) so misconfig fails fast
  before the first request.

Consumer refactors (~12 services + routes), each parametrized over the
abstraction:
- imageProcessor / videoProcessor — pipe Sharp/ffmpeg output through
  storage.put; expose withLocalCopy() helper for S3-mode regeneration
  paths that need a local file for sharp/ffmpeg.
- archiveService / downloadZipService — finalize zip in tmp dir, then
  storage.putFromFile. Atomic-rename pattern preserved on local; S3
  emulates via copy + delete (worker prunes orphaned .tmp.* on startup).
- photoProcessor / photoReplacementService / adminPhotos upload+delete /
  routes/v1/events.js POST /events/:id/photos / routes/events.js — every
  upload path now goes storage.putFromFile(temp) → unlink temp.
- gallery.js bulk-download (cached + on-the-fly + selected) — managed
  photos via storage.get, external-mode unchanged.
- protectedImages / secureImages / photoResolver — read via
  storage.get; resolvePhotoStorageKey returns the canonical key.
- watermarkService / watermarkGeneratorService — persistent watermarks
  via storage.put.
- fileWatcher — bails out with a clear log warning when STORAGE_BACKEND=s3
  (chokidar can't watch S3); auto-import lands via the S3 prefix walker
  introduced in the follow-up commit.
- expirationChecker — small touch (event.expired webhook fire from #327
  shipping in the next commit).

Migration tooling
- backend/scripts/migrate-storage.js — one-shot --dry-run capable script
  that walks photos.path, thumbnail_path, hero_path, watermark_path and
  events.archive_path/download_zip_path; streams local → S3; sha256
  size-match skip for idempotent re-run; failures CSV.

Presigned-URL "Download All" (#328 follow-up shipped in this commit)
- routes/gallery.js — when STORAGE_BACKEND=s3 + event.allow_presigned_download
  + downloads enabled + watermark NOT enabled, /download-all returns a
  302 redirect to a 5-minute presigned S3 URL. Per-event opt-in surface
  ships in the next commit's UI.

Tests
- backend/__tests__/integration/storageBackend.test.js — parametrized
  contract suite running against BOTH LocalFs AND MinIO (18 tests, both
  backends — 36 cases total).
- backend/__tests__/integration/imageProcessor.storage.test.js — same
  parametrized pattern for the image processor (10 tests × 2 backends).
- backend/__tests__/integration/backup-s3.test.js — bootstrap fix:
  drop the redundant initDb() (001_init handles it) and remove
  schema-drift in configureS3Backup (app_settings has no created_at
  anymore and the unique constraint is on setting_key alone, not
  composite). 0/12 → 7/12 (5 remaining are unrelated assertion drift).
- backend/src/services/photoResolver.js — mixed-source events (reference
  mode with managed-uploaded photos) now fall back to managed when
  external_relpath is missing instead of throwing.
- tests/e2e/s3-storage-roundtrip.spec.ts — Playwright spec that
  auto-skips against local backend; full upload → serve → delete
  round-trip when run against an S3-mode backend.

Server wiring (server.js)
- initStorage() called after database init, before rate limiters.
- This commit's diff also includes the webhook delivery worker startup
  and the S3 auto-importer startup. Those features ship in the next two
  commits — co-located here for one bisectable diff per file.

Docs + ops
- README §"Storage Backends" — capability matrix, switching playbook,
  IAM policy snippet, MinIO/R2/B2 examples.
- README §"Webhooks" — also added here (full diff bundled).
- .env.example — STORAGE_BACKEND + STORAGE_S3_* + STORAGE_AUTO_IMPORT
  documented; WEBHOOK_* added in the same diff.
- .gitignore — re-anchor the existing `storage/` rule to `/storage/`
  so backend/src/services/storage/ (the new abstraction code) is
  trackable. The runtime ./storage/ data dir stays ignored.

Out of scope for v1 (per the issue): presigned URLs for individual
photo display (always streamed for protection middleware), CDN
integration, hybrid hot/cold tiers, S3 → local migration, multi-bucket
per-event.
2026-04-28 10:06:36 +02:00
Paul Nothaft 46bc894d91 docs: add Buy Me a Coffee badge + Support section
Adds a yellow Buy Me a Coffee badge to the header alongside the existing
License/Docker/Node/React badges, plus a small "Support the Project"
section above Acknowledgments with the standard BMC button image. Also
adds a link in the inline nav row at the top of the README so first-time
visitors can find it.

Link: https://buymeacoffee.com/theluap

Lightweight, opt-in support — explicitly notes that starring, sharing,
filing good bug reports, and opening PRs are equally welcome ways to
help if money isn't in the budget.
2026-04-27 22:38:00 +02:00
Paul Nothaft cd00bc13d4 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:27:21 +01:00
Paul Nothaft a4c624802b fix: update docker-compose to docker compose and add ADMIN_PASSWORD to .env.example (#189)
- Replace deprecated docker-compose (v1) with docker compose (v2) in README
- Add missing ADMIN_PASSWORD to .env.example so new users don't get a
  blank-string warning and can actually log in after first setup
2026-02-21 08:28:49 +01:00
Paul Nothaft 6c30e2c2ed feat: add category hero/cover photo selection (#163)
Wire up the hero_photo_id column on photo_categories that was added in
the migration but never connected. Backend routes now accept and persist
hero_photo_id on category create/update, a dedicated PUT /:id/hero
endpoint is added, and the gallery API returns hero_photo_id for each
category. Frontend EventCategoryManager shows a clickable thumbnail per
category that opens a photo picker modal. Includes EN/DE i18n keys.
2026-02-03 15:43:23 +01:00
Paul Nothaft 617e778a48 feat: implement beta/stable release channels with update notifications
Add dual-channel release strategy for stable and beta releases:

Release Channels:
- Stable channel: production-ready releases (stable, latest, v2.3.0)
- Beta channel: early access features (beta, v2.3.0-beta.1)
- Configurable via PICPEAK_CHANNEL environment variable

Update Notifications:
- Admin dashboard shows available updates for configured channel
- Checks GitHub Releases API with 1-hour cache
- Can be disabled with UPDATE_CHECK_ENABLED=false

CI/CD Changes:
- New release-please-beta.yml workflow for beta prereleases
- Docker build workflow produces stable/beta tags based on branch
- Beta versions use v2.3.0-beta.1 format

New Files:
- .github/workflows/release-please-beta.yml
- release-please-config-beta.json
- .release-please-manifest-beta.json
- backend/src/services/updateCheckService.js
- frontend/src/components/admin/UpdateNotification.tsx

Modified Files:
- docker-compose.production.yml (channel selection)
- .env.example (PICPEAK_CHANNEL, UPDATE_CHECK_ENABLED)
- backend/src/routes/adminSystem.js (/updates endpoint)
- frontend components (VersionInfo, AdminDashboard)
- i18n locales (en.json, de.json)
- README.md and DEPLOYMENT_GUIDE.md (documentation)
2026-01-15 12:11:06 +01:00
Paul Nothaft 1931d73b60 fix: resolve branding display issues and invitation parsing errors
Fixes #84 - Logo and favicon not displaying on branding page and galleries
Fixes #85 - Invitations showing undefined expiresAt causing parseISO errors

Changes:
- Fix nginx.conf: Add ^~ modifier to /uploads location to prioritize proxy over static file matching
- Fix vite.config.ts: Add /uploads proxy for development environment
- Fix BrandingPage.tsx: Include logo_url from branding settings instead of expecting it from theme
- Fix adminUsers.js: Add transformInvitation() to convert snake_case DB fields to camelCase API response
- Fix publicSettings.js: Add branding_hide_powered_by to public settings API response
- Update README.md: Mark Multiple Administrators feature as implemented
- Bump version to 2.2.1
2026-01-08 13:56:02 +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
Claude 0d95eab86a Add chunked upload support for large video files up to 10GB
- Increased max file size from 500MB to 10GB
- Created chunkedUploadService.js for managing chunked uploads
- Added chunked upload API endpoints (init, chunk, complete, status, abort)
- Added frontend chunked upload methods to photos.service.ts
- Files >100MB automatically use chunked uploads
- 10MB chunk size for reliable transfers
- Auto-cleanup of expired uploads after 24 hours
- Updated README with 10GB limit and nginx configuration example
2025-11-28 17:53:56 +01:00
Claude f3482a9a78 Update README with video support requirements and status
- Added Video Support Requirements section with resource recommendations
- Noted FFmpeg is bundled via npm (no system installation required)
- Listed supported formats and max file size
- Updated roadmap to mark Video Support as implemented
2025-11-28 17:53:56 +01:00
Paul Nothaft 775c5159ea Add customer contact fields and admin API docs (refs #41) 2025-10-14 18:29:21 +02:00
paul 2a4d38813f feat: overhaul public landing page and backup tooling 2025-09-19 16:39:18 +02:00
paul 097ce2c205 Fix native install schema gaps (closes #20)
Mirror to GitHub / mirror (push) Successful in 1m38s
Test and Lint / backend-test (push) Successful in 1m44s
Test and Lint / frontend-test (push) Successful in 2m12s
Version and Release / version-bump (push) Failing after 1m20s
Version and Release / trigger-drone (push) Has been skipped
2025-09-18 12:42:05 +02:00
paul 0178e71c67 docs: add PUID/PGID note for Docker bind mounts to avoid permission issues
Mirror to GitHub / mirror (push) Successful in 40s
Test and Lint / backend-test (push) Successful in 1m31s
Test and Lint / frontend-test (push) Successful in 2m16s
2025-09-14 16:10:46 +02:00
paul ee13556c5c docs(readme): reflect new External Media reference mode and update roadmap (gallery feedback status)
Mirror to GitHub / mirror (push) Successful in 36s
Test and Lint / backend-test (push) Successful in 1m31s
Test and Lint / frontend-test (push) Successful in 2m14s
2025-09-06 10:16:08 +02:00
paul 6de64a1df1 fix: resolve port configuration issues and database column mismatch
Mirror to GitHub / mirror (push) Failing after 40s
Test and Lint / backend-test (push) Successful in 1m40s
Test and Lint / frontend-test (push) Successful in 2m1s
Version and Release / version-bump (push) Successful in 1m6s
Version and Release / trigger-drone (push) Successful in 3s
- Fixed database query in adminDashboard.js using non-existent 'created_at' column
  Changed to use 'scheduled_at' for email_queue table queries
- Updated frontend/.env.example to default to Docker configuration (port 3001/api)
- Clarified DEPLOYMENT_GUIDE.md with separate frontend/backend configuration sections
- Added explicit port configuration warnings to prevent future mismatches
- Added beta features section to README for download protection and deployment script

The 500 errors were caused by:
1. Frontend .env pointing to wrong port (3002 instead of 3001)
2. Database query using 'created_at' instead of 'scheduled_at' for email_queue

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-24 11:11:53 +02:00
paul 6389b9df3f fix: update all deployment guide links in README.md
Mirror to GitHub / mirror (push) Successful in 28s
Test and Lint / backend-test (push) Successful in 1m18s
Test and Lint / frontend-test (push) Successful in 1m56s
continuous-integration/drone/push Build is passing
- Change all links from DEPLOYMENT.md to DEPLOYMENT_GUIDE.md
- Fixed 3 occurrences: documentation section, getting started section, and footer
- Matches the actual filename in the repository

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-26 00:25:04 +02:00
paul 0c989ce086 docs: replace email addresses with GitHub issue links
Mirror to GitHub / mirror (push) Successful in 32s
Test and Lint / backend-test (push) Successful in 1m35s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 1m59s
- Remove all @example.com email addresses from documentation
- Replace security@example.com with GitHub security issue links
- Replace conduct@example.com with GitHub issue link
- Update CONTRIBUTING.md to use GitHub issues instead of email
- Ensure all communication happens through GitHub's issue tracking system
- Avoid direct email communication for better transparency and tracking
2025-07-24 21:28:44 +02:00
paul 35e360dcf7 docs: add transparency note about AI-assisted development
Mirror to GitHub / mirror (push) Successful in 33s
Test and Lint / backend-test (push) Successful in 1m30s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m4s
Version and Release / version-bump (push) Successful in 43s
Version and Release / trigger-drone (push) Has been skipped
- Add acknowledgment section about AI generation
- Clarify human testing and security auditing
- Emphasize production testing and code review
- Remove unnecessary .gitkeep files
2025-07-24 21:19:57 +02:00