From 27dedb13f390f956690e667d41ed532bce697eb4 Mon Sep 17 00:00:00 2001 From: Luca <102960244+Luca-Timo@users.noreply.github.com> Date: Tue, 11 Aug 2026 11:38:35 +0300 Subject: [PATCH] docs: flip README links to docs.picpeak.app + delete docs/_to-migrate (#1000 phase 3) (#1023) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- README.md | 24 +- backend/Dockerfile | 2 +- backend/server.js | 2 +- backend/src/routes/adminFeatureFlags.js | 2 +- docs/_to-migrate/README.md | 22 -- docs/_to-migrate/first-run-setup.md | 26 -- docs/_to-migrate/roadmap.md | 28 -- docs/_to-migrate/storage-backends.md | 22 -- docs/_to-migrate/system-requirements.md | 63 ---- docs/_to-migrate/webhooks.md | 77 ---- docs/accounting-inbound-invoices.md | 97 ----- docs/backup-restore.md | 350 ------------------ docs/crm-disclaimers.md | 164 -------- docs/fonts.md | 106 ------ docs/live-slideshow.md | 74 ---- frontend/src/i18n/locales/de.json | 2 +- frontend/src/i18n/locales/en.json | 2 +- .../admin/contracts/ContractEditorPage.tsx | 2 +- .../settings/SettingsBusinessProfilePage.tsx | 2 +- frontend/src/services/featureFlags.service.ts | 2 +- 20 files changed, 20 insertions(+), 1049 deletions(-) delete mode 100644 docs/_to-migrate/README.md delete mode 100644 docs/_to-migrate/first-run-setup.md delete mode 100644 docs/_to-migrate/roadmap.md delete mode 100644 docs/_to-migrate/storage-backends.md delete mode 100644 docs/_to-migrate/system-requirements.md delete mode 100644 docs/_to-migrate/webhooks.md delete mode 100644 docs/accounting-inbound-invoices.md delete mode 100644 docs/backup-restore.md delete mode 100644 docs/crm-disclaimers.md delete mode 100644 docs/fonts.md delete mode 100644 docs/live-slideshow.md diff --git a/README.md b/README.md index 76aae2e6..441b6feb 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ docker compose up -d # Access at http://localhost:3000 ``` -On first start, open **http://localhost:3000/admin** and follow the in-browser setup to create your admin account. Full details — the one-time setup token, Docker file permissions, and ARM64 notes — are in **[First-run setup](docs/_to-migrate/first-run-setup.md)**. +On first start, open **http://localhost:3000/admin** and follow the in-browser setup to create your admin account. Full details — the one-time setup token, Docker file permissions, and ARM64 notes — are in **[First-run setup](https://docs.picpeak.app/getting-started/first-login)**. > **Updating / release channels:** set `PICPEAK_CHANNEL` (`stable` default, or `beta`) in `.env`, then `docker compose pull && docker compose up -d`. See [RELEASING.md](RELEASING.md) for the promotion cadence. @@ -80,11 +80,11 @@ Unlike expensive SaaS solutions, PicPeak gives you: ## ✨ Features -**For photographers** — drag & drop upload, auto-expiring & password-protected galleries, automated emails, an analytics dashboard, custom themes, a public landing page, and a [Live Slideshow](docs/live-slideshow.md) projector view that auto-picks-up new uploads during live events. +**For photographers** — drag & drop upload, auto-expiring & password-protected galleries, automated emails, an analytics dashboard, custom themes, a public landing page, and a [Live Slideshow](https://docs.picpeak.app/features/live-slideshow) projector view that auto-picks-up new uploads during live events. **For clients** — clean mobile-optimized galleries, one-click bulk downloads, smart search, optional guest uploads, and download protection (watermarking + right-click prevention). -**Technical** — Docker-ready, automatic thumbnail generation, external media reference mode, smart archiving of expired galleries, S3-compatible [storage backends](docs/_to-migrate/storage-backends.md), [webhooks](docs/_to-migrate/webhooks.md), and security-first defaults (JWT, rate limiting, CORS). +**Technical** — Docker-ready, automatic thumbnail generation, external media reference mode, smart archiving of expired galleries, S3-compatible [storage backends](https://docs.picpeak.app/features/storage-backends), [webhooks](https://docs.picpeak.app/features/webhooks), and security-first defaults (JWT, rate limiting, CORS).
🧾 For studios — CRM & Accounting (Beta, off by default) @@ -98,7 +98,7 @@ Unlike expensive SaaS solutions, PicPeak gives you:
> [!WARNING] -> **CRM & Accounting — examples only, verify locally.** Feature-flagged off by default. Seeded contract blocks are written by the maintainer, **not a lawyer**; QR-bills/SEPA payloads and every tax, VAT and Treuhänder/Banana figure are computed from your input and defaults and are **jurisdiction-specific guidance only**. Have your lawyer review contracts, scan a test QR with your bank's app, and verify all numbers with your accountant / Treuhänder / tax authority before customer-facing use. Read **[docs/crm-disclaimers.md](docs/crm-disclaimers.md)** first. +> **CRM & Accounting — examples only, verify locally.** Feature-flagged off by default. Seeded contract blocks are written by the maintainer, **not a lawyer**; QR-bills/SEPA payloads and every tax, VAT and Treuhänder/Banana figure are computed from your input and defaults and are **jurisdiction-specific guidance only**. Have your lawyer review contracts, scan a test QR with your bank's app, and verify all numbers with your accountant / Treuhänder / tax authority before customer-facing use. Read **[the CRM disclaimers](https://docs.picpeak.app/features/crm/disclaimers)** first. ## 📖 Documentation @@ -109,14 +109,14 @@ Full documentation lives at **[docs.picpeak.app](https://docs.picpeak.app)** — | 🚀 Deployment (Docker, env, reverse proxy, SSL) | [docs.picpeak.app/deployment](https://docs.picpeak.app/deployment) | | ⚙️ Admin settings reference | [docs.picpeak.app/guides/admin-settings](https://docs.picpeak.app/guides/admin-settings) | | 🎯 Creating events | [docs.picpeak.app/guides/creating-events](https://docs.picpeak.app/guides/creating-events) | -| 📽️ Live Slideshow | [docs/live-slideshow.md](docs/live-slideshow.md) | -| 💾 Backup & Restore | [docs/backup-restore.md](docs/backup-restore.md) | +| 📽️ Live Slideshow | [docs.picpeak.app/features/live-slideshow](https://docs.picpeak.app/features/live-slideshow) | +| 💾 Backup & Restore | [docs.picpeak.app/guides/backup-restore](https://docs.picpeak.app/guides/backup-restore) | | 🔌 API reference | [docs.picpeak.app/api](https://docs.picpeak.app/api) | -| 🪝 Webhooks | [docs/_to-migrate/webhooks.md](docs/_to-migrate/webhooks.md) | -| 💾 Storage backends (local / S3) | [docs/_to-migrate/storage-backends.md](docs/_to-migrate/storage-backends.md) | -| 💻 System requirements & tuning | [docs/_to-migrate/system-requirements.md](docs/_to-migrate/system-requirements.md) | -| 🧾 CRM & Accounting | [docs.picpeak.app/features/crm](https://docs.picpeak.app/features/crm) · [disclaimers](docs/crm-disclaimers.md) | -| 🗺️ Roadmap | [docs/_to-migrate/roadmap.md](docs/_to-migrate/roadmap.md) | +| 🪝 Webhooks | [docs.picpeak.app/features/webhooks](https://docs.picpeak.app/features/webhooks) | +| 💾 Storage backends (local / S3) | [docs.picpeak.app/features/storage-backends](https://docs.picpeak.app/features/storage-backends) | +| 💻 System requirements & tuning | [docs.picpeak.app/deployment/system-requirements](https://docs.picpeak.app/deployment/system-requirements) | +| 🧾 CRM & Accounting | [docs.picpeak.app/features/crm](https://docs.picpeak.app/features/crm) · [disclaimers](https://docs.picpeak.app/features/crm/disclaimers) | +| 🗺️ Roadmap | [GitHub Issues](https://github.com/PicPeak/picpeak/issues) | **Project meta:** [Contributing](CONTRIBUTING.md) · [License](LICENSE) · [Security](SECURITY.md) · [Code of Conduct](CODE_OF_CONDUCT.md) @@ -141,7 +141,7 @@ Full documentation lives at **[docs.picpeak.app](https://docs.picpeak.app)** — - **Backend**: Node.js, Express, SQLite/PostgreSQL - **Frontend**: React, Tailwind CSS, Framer Motion -- **Storage**: Local filesystem (default) or S3-compatible object store (AWS S3, MinIO, R2, B2, Wasabi, Spaces) — see [Storage Backends](docs/_to-migrate/storage-backends.md) +- **Storage**: Local filesystem (default) or S3-compatible object store (AWS S3, MinIO, R2, B2, Wasabi, Spaces) — see [Storage Backends](https://docs.picpeak.app/features/storage-backends) - **Email**: SMTP with customizable templates - **Analytics**: Privacy-focused with Umami integration - **External media**: point PicPeak at `EXTERNAL_MEDIA_ROOT` to reference existing originals read-only, index quickly, and generate thumbnails on demand diff --git a/backend/Dockerfile b/backend/Dockerfile index 406aaedd..7ecd9ae3 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -68,7 +68,7 @@ RUN rm -rf /usr/local/lib/node_modules/npm /usr/local/bin/npm /usr/local/bin/npx # PDFs to flat PNGs server-side so the admin UI NEVER renders a raw (possibly # malicious) PDF. pdftoppm does not execute embedded JS or fetch remote # resources, so it doubles as the SSRF/phone-home guard for untrusted inbound -# documents (see docs/accounting-inbound-invoices.md). +# documents (see https://docs.picpeak.app/features/accounting/incoming-invoices). # exiftool extracts the embedded full-res JPEG preview from RAW/DNG uploads # (Apple ProRAW etc.) — sharp's libvips has no raw loader, so the pipeline # thumbnails/displays that preview while keeping the original for download. diff --git a/backend/server.js b/backend/server.js index 1b41b3ec..e93f33d4 100644 --- a/backend/server.js +++ b/backend/server.js @@ -497,7 +497,7 @@ app.use('/uploads', setCorsHeaders, secureStatic(path.join(storagePath, 'uploads // are stable (e.g. Inter/400.woff2), so an admin replacing the file on disk // must be able to roll out the change to clients. With max-age + Last-Modified // (set by express.static from file mtime), browsers send If-Modified-Since -// after expiry and pick up the new version automatically. See docs/fonts.md +// after expiry and pick up the new version automatically. See https://docs.picpeak.app/guides/custom-fonts // "Replacing an existing font" for the documented rollout strategy. const fontStaticOpts = { maxAge: '7d' }; app.use( diff --git a/backend/src/routes/adminFeatureFlags.js b/backend/src/routes/adminFeatureFlags.js index 9555db84..9e333407 100644 --- a/backend/src/routes/adminFeatureFlags.js +++ b/backend/src/routes/adminFeatureFlags.js @@ -63,7 +63,7 @@ const KNOWN_FLAGS = [ // are a standalone legal document type with their own composition // (blocks) and signing flow (in-browser canvas + wet-signed PDF // upload). Seeded block bodies are EXAMPLES ONLY; admins must have a - // lawyer review before sending. See docs/crm-disclaimers.md. + // lawyer review before sending. See https://docs.picpeak.app/features/crm/disclaimers. 'contracts', // Accounting (migration 122). Top-level Accounting area — inbound // supplier invoices, expenses + re-bill, and the tax report (which diff --git a/docs/_to-migrate/README.md b/docs/_to-migrate/README.md deleted file mode 100644 index d63301d4..00000000 --- a/docs/_to-migrate/README.md +++ /dev/null @@ -1,22 +0,0 @@ -# `_to-migrate` — staging area, **not canonical** - -> [!WARNING] -> These pages were cut out of the root `README.md` to slim it down. They are -> **being ported to the PicPeak docs site** (its own repo → docs.picpeak.app). -> -> - **Do not edit** these files expecting the change to be authoritative — edit -> the docs-site copy once it exists. -> - **Do not add new links** to this folder from code or other docs. -> - This whole folder is **deleted** once the docs-site pages are live (see the -> 3-phase plan in PicPeak/picpeak#1000). - -The root README links here only as a temporary bridge so no link 404s while the -docs-site pages are being written. - -| File | Ported to docs.picpeak.app path | -|---|---| -| `webhooks.md` | `/features/webhooks` | -| `storage-backends.md` | `/deployment` (storage section) | -| `first-run-setup.md` | `/deployment` (first run / permissions) | -| `system-requirements.md` | `/deployment` (system requirements) | -| `roadmap.md` | project roadmap / GitHub Projects (TBD) | diff --git a/docs/_to-migrate/first-run-setup.md b/docs/_to-migrate/first-run-setup.md deleted file mode 100644 index 14fd38ac..00000000 --- a/docs/_to-migrate/first-run-setup.md +++ /dev/null @@ -1,26 +0,0 @@ -# First run — create your admin account - -On first start with no `ADMIN_PASSWORD` set, PicPeak has **no admin account yet** and greets you with an in-browser setup screen — no credentials in `.env`: - -1. Open **http://localhost:3000/admin** — you'll be redirected to `/setup`. -2. Read the **one-time setup token** from the 0600 file the backend writes it to - (it is deliberately *not* printed to the logs — that would leave a live - bootstrap credential in `docker logs`): - ```bash - docker compose exec backend cat /app/data/SETUP_TOKEN - ``` - It is bind-mounted, so `sudo cat data/SETUP_TOKEN` on the host works too. Only - if that file could not be written does the backend fall back to logging the - token (`docker compose logs backend | grep -i "setup token"`). -3. Paste the token, set your admin **email + password**, and you're in. The token is single-use, and the setup screen closes permanently once an admin exists. - -> Prefer the old behaviour? Set `ADMIN_PASSWORD` in `.env` and PicPeak auto-creates the admin on first boot instead (credentials written to `data/ADMIN_CREDENTIALS.txt`). - -## Docker file permissions - -- The backend container starts as root, chowns bind-mounted host directories (`./storage`, `./data`, `./logs`) to UID 1001 (`nodejs`), then drops privileges via `su-exec` before running the app. No host-side setup needed for fresh installs. -- If you pin `user:` in a compose override (e.g. to map a specific host UID), the self-chown is skipped and you must pre-chown the host directories to that UID — see [docs.picpeak.app/deployment/docker#permissions](https://docs.picpeak.app/deployment/docker#permissions). - -## ARM64 (aarch64) systems - -Pre-built images include native `linux/arm64`, no platform flags or emulation needed. If you're on an older image tag that's still amd64-only, see [docker-compose.amd64.override.yml](../../docker-compose.amd64.override.yml) for a transitional fallback. diff --git a/docs/_to-migrate/roadmap.md b/docs/_to-migrate/roadmap.md deleted file mode 100644 index 6002839b..00000000 --- a/docs/_to-migrate/roadmap.md +++ /dev/null @@ -1,28 +0,0 @@ -# Roadmap - -We're constantly improving PicPeak and welcome contributions from our community! If you have ideas for new features or want to help implement existing ones, please open an issue or submit a pull request. Your contributions help make PicPeak better for everyone. - -## 🚧 Beta Features (Use at your own risk) - -These features are currently in beta testing and may have limited functionality or stability: - -| Feature | Description | Status | -|---------|-------------|--------| -| **CRM & Accounting Module** | Quotes, contracts, invoices (+ Storno), hours logging, calendar, and tax report — plus inbound supplier-invoice capture, internal expenses, and a Treuhänder/Banana (Swiss/LI) accountant-journal export. Feature-flagged off by default. Seeded contract blocks, payment terms, IBAN / QR-bill and tax defaults are **examples only** and need legal / financial / **tax** review before customer-facing use. See [docs.picpeak.app/features/crm](https://docs.picpeak.app/features/crm). | 🧪 Beta | -| **Simple Deployment Script** | One-click deployment script for quick server setup with automated configuration and dependency installation | 🧪 Beta | - -## 📋 Future Enhancements - -| Feature | Description | Priority | Status | -|---------|-------------|----------|---------| -| **Backup & Restore** | Comprehensive backup system with S3/MinIO support, automated scheduling, and safe restore functionality | High | ✅ Implemented | -| **External Media Library (Reference Mode)** | Use an external folder library as a read‑only source with import and on‑demand thumbnail generation | High | ✅ Implemented | -| **Download Protection** | Advanced image protection system with canvas rendering, invisible watermarking, right-click prevention, and DevTools detection to protect photos from unauthorized downloads | High | ✅ Implemented | -| **Gallery Templates** | Multiple gallery layouts (grid, masonry, carousel, timeline, hero, mosaic) with custom CSS styling support. Includes starter templates like Apple Liquid Glass for complete visual customization | Medium | ✅ Implemented | -| **Face Recognition** | AI-powered face detection to help guests find their photos and create automatic person-based albums | Low | 🔄 Open | -| **Gallery Feedback** | Allow guests to like, rate, and comment on photos with admin notifications and moderation | Medium | ✅ Implemented | -| **Video Support** | Upload and display videos alongside photos in galleries with streaming support | Low | ✅ Implemented | -| **Multiple Administrators** | Support for multiple admin accounts with role-based permissions and activity tracking | Low | ✅ Implemented | -| **Filtering & Export Options** | Filter photos by likes, ratings, comments, or favorites. Search by filename. Sort by date, name, size, or rating. Export filtered selections as ZIP or generate Capture One/Lightroom-compatible file lists for professional workflows | Medium | ✅ Implemented | - -**Status Legend:** ✅ Implemented | 🚧 In Progress | 🔄 Open | 📋 Planned diff --git a/docs/_to-migrate/storage-backends.md b/docs/_to-migrate/storage-backends.md deleted file mode 100644 index d6afc191..00000000 --- a/docs/_to-migrate/storage-backends.md +++ /dev/null @@ -1,22 +0,0 @@ -# Storage Backends - -PicPeak supports two storage backends for photos, thumbnails, hero images, watermarks, and archive zips. Both are configured via environment variables; no code change is required to switch. - -| Capability | `STORAGE_BACKEND=local` (default) | `STORAGE_BACKEND=s3` | -|---|---|---| -| Photo / thumbnail / hero storage | Local filesystem under `STORAGE_PATH` | Bucket on any S3-compatible service | -| Admin UI upload | ✅ | ✅ | -| Filesystem auto-import (chokidar watcher) | ✅ | ❌ — disabled (use the upload API) | -| Watermarks, fingerprinting, fragmentation | ✅ | ✅ (materialized to a tmp file just-in-time) | -| Bulk download zips (cached + on-the-fly) | ✅ | ✅ | -| Backups | ✅ | ✅ | -| External media reference mode (`EXTERNAL_MEDIA_ROOT`) | ✅ (always local) | ✅ (still local — not migrated) | - -## Switching to an S3-compatible backend - -1. Provision a bucket and credentials. The minimum IAM policy is documented in `.env.example`. -2. Set `STORAGE_BACKEND=s3` plus `STORAGE_S3_BUCKET`, `STORAGE_S3_REGION`, `STORAGE_S3_ACCESS_KEY`, `STORAGE_S3_SECRET_KEY`. For non-AWS providers (MinIO, R2, B2, …) also set `STORAGE_S3_ENDPOINT`. -3. If you have existing local content, copy it first: `node backend/scripts/migrate-storage.js --dry-run` then `node backend/scripts/migrate-storage.js`. The script is idempotent and writes a failures CSV. -4. Restart the backend. The startup check pings the bucket and refuses to boot on misconfig. - -Note: presigned-URL serving (zero-bandwidth direct downloads from S3) is intentionally **not** in v1 — every request still streams through the backend so watermarks, devtools-detection, and access logging keep working. diff --git a/docs/_to-migrate/system-requirements.md b/docs/_to-migrate/system-requirements.md deleted file mode 100644 index 33984cfa..00000000 --- a/docs/_to-migrate/system-requirements.md +++ /dev/null @@ -1,63 +0,0 @@ -# System Requirements - -## Minimum Requirements -- **CPU**: 2 CPU cores -- **RAM**: **4 GB minimum** for a normal photo-upload workload — sharp/libvips - decodes the full uncompressed frame before resize, and the default two - worker loops at sharp-concurrency 2 can push peak RSS past 1.5 GB on a - batch of 20-MP+ photos. On a 2 GB VPS that's enough to OOM-kill the - backend mid-batch (surfaces as 503s on thumbnails — see [Low-memory - hosts](#low-memory-hosts) below for the recipe to run on 2 GB). -- **Storage**: 20GB minimum (plus photo storage needs) -- **OS**: Linux (Ubuntu 20.04+), macOS, or Windows with WSL2 -- **Node.js**: v18.0.0 or higher -- **Database**: SQLite (included) or PostgreSQL 12+ - -## Docker Requirements (Recommended) -- **Docker**: v20.10.0+ -- **Docker Compose**: v2.0.0+ - -## Low-memory hosts - -Running on 2 GB RAM (e.g. an entry-level VPS) is workable but requires -tuning the upload-processor concurrency down. The backend auto-detects -total RAM at startup via `os.totalmem()` — on a host that reports < 3 GB, -it defaults `UPLOAD_PROCESSOR_CONCURRENCY` to **1** instead of 2 and logs -a one-shot warning. You can pin the value explicitly in `.env`: - -```env -# Single worker loop — slower batch processing, lower peak RSS -UPLOAD_PROCESSOR_CONCURRENCY=1 -``` - -The trade-off is throughput: a single worker processes one photo at a -time, so a 100-photo batch takes ~2× as long but won't OOM. **Health-check -note**: if the backend dies under memory pressure, the gallery serves -`503 Service Unavailable` on thumbnails until Docker's -`restart: unless-stopped` brings the container back. Persistent 503s -during/after an upload batch on a low-memory host are almost always this. - -## Video Support Requirements -When enabling video uploads, consider these additional resources: - -| Resource | Recommendation | Notes | -|----------|----------------|-------| -| **RAM** | 4GB+ recommended | FFmpeg processing requires more memory | -| **Storage** | Plan for 10-100x more | Videos are significantly larger than images | -| **CPU** | Additional cores help | Video thumbnail extraction is CPU-intensive | -| **Bandwidth** | Higher throughput | Video streaming requires more bandwidth | - -**Technical Notes:** -- FFmpeg is bundled via npm (`@ffmpeg-installer/ffmpeg`) - no system installation required -- Maximum upload size: **10GB per video file** -- Chunked upload support for files >100MB (resumable uploads) -- Supported formats: MP4, WebM, MOV, AVI -- Video thumbnails are automatically generated from the first few seconds - -**For Nginx/Reverse Proxy:** -If using Nginx, increase the client max body size: -```nginx -client_max_body_size 10G; -proxy_read_timeout 3600; -proxy_send_timeout 3600; -``` diff --git a/docs/_to-migrate/webhooks.md b/docs/_to-migrate/webhooks.md deleted file mode 100644 index bb497018..00000000 --- a/docs/_to-migrate/webhooks.md +++ /dev/null @@ -1,77 +0,0 @@ -# Webhooks - -PicPeak POSTs event/photo lifecycle notifications to URLs you configure under **Settings → Webhooks**. Each delivery is signed `HMAC-SHA256` with a per-webhook secret in the `X-PicPeak-Signature` header so receivers can verify the request really came from your PicPeak instance. - -## Event types - -| Event | Fires when | -|---|---| -| `event.created` | Gallery created (admin or API) | -| `event.published` | Draft becomes live (`is_draft: true → false`) — also fires when an event is created with `is_draft=false` | -| `event.archived` | Bulk-archive, manual archive, or auto-archive on expiry | -| `event.expired` | Expiration checker marks the gallery inactive (fires before `event.archived` in the cascade) | -| `photo.uploaded` | Admin upload, API upload, guest upload, or auto-import | -| `photo.deleted` | Single delete, bulk delete (NOT fired per-photo when an event is archived — receivers infer from `event.archived` to avoid flooding) | - -## Payload shape - -```json -{ - "id": "delivery-uuid", - "type": "event.published", - "created_at": "2026-04-28T05:25:00.000Z", - "data": { - "event": { "id": 123, "slug": "wedding-smith", "share_url": "https://..." } - } -} -``` - -Also sent on every request: -- `X-PicPeak-Signature` — `HMAC-SHA256(secret, raw_body)` as hex -- `X-PicPeak-Event` — the event type (handy for routing without parsing the body) -- `X-PicPeak-Delivery` — UUID for idempotency on the receiver side -- `User-Agent: PicPeak-Webhooks/1.0` - -## Verifying signatures - -**Node.js** -```js -const crypto = require('crypto'); -function verify(secret, rawBody, signature) { - const expected = crypto.createHmac('sha256', secret).update(rawBody).digest('hex'); - const a = Buffer.from(expected, 'hex'); - const b = Buffer.from(signature, 'hex'); - if (a.length !== b.length) return false; - return crypto.timingSafeEqual(a, b); -} -``` - -**Python** -```python -import hmac, hashlib -def verify(secret: str, raw_body: bytes, signature: str) -> bool: - expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest() - return hmac.compare_digest(expected, signature) -``` - -**curl + openssl** (one-liner for a quick replay) -```sh -SIG=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print $2}') -[ "$SIG" = "$RECEIVED_SIG" ] && echo OK || echo MISMATCH -``` - -## Retries + observability - -- `2xx` → success, recorded with latency -- Non-`2xx` or network error → exponential backoff: `1m → 5m → 30m → 2h → 12h`, max 5 attempts -- After max attempts: status `failed`, surfaces in **Settings → Webhooks → Deliveries** with a "Replay" button -- Up to 5 deliveries in flight at once; one slow consumer can't block others (configurable via `WEBHOOK_DELIVERY_CONCURRENCY`) -- Response body truncated to 1KB before storage so chatty receivers don't bloat the audit log - -The deliveries page (`/admin/webhooks/:id/deliveries`) shows every attempt with timestamp, status, HTTP code, latency, payload sent, signature, and response. Click "Send test event" to fire a synthetic delivery for any event type. - -## SSRF protection - -Webhook URLs are validated against the same private-IP blocklist used elsewhere in the app — loopback, private RFC1918 ranges, link-local, `.local`/`.internal` hostnames, cloud metadata endpoints. The check runs both at create time and per-delivery (DNS-rebinding mitigation). - -For local development with a receiver on the same machine or docker network, set `WEBHOOK_ALLOW_PRIVATE_URLS=true`. Production deployments must leave this OFF. diff --git a/docs/accounting-inbound-invoices.md b/docs/accounting-inbound-invoices.md deleted file mode 100644 index 51a9d9a8..00000000 --- a/docs/accounting-inbound-invoices.md +++ /dev/null @@ -1,97 +0,0 @@ -# Accounting — Incoming invoices, expenses & re-bill - -> **Status:** built on `feat/accounting-inbound-invoices` (based on `upstream/beta`); not yet merged to `main`. -> **Legal:** every VAT / tax-treatment surface is an *example only* and must be reviewed with a Treuhänder before relying on it. Jurisdiction scope is **Liechtenstein-first** (Swiss/LI rails — QR-bill, LI MWST), not German DATEV/ELSTER/ITSG. See `docs/crm-disclaimers.md`. - -## Why -The studio receives supplier invoices/receipts (hotels, equipment, Fremdleistungen). This feature lets an admin **capture** an incoming invoice (upload, **phone/tablet camera**, or **IMAP email intake**), confirm its fields, give it a **disposition**, mark the **supplier payable** paid, and — for client-borne costs — **re-bill it to a client** ("Weiterverrechnung"), consolidated onto the client's bill the same way billable hours are. - -## Two distinct entities (split in migration 126) -Incoming invoices and internal expenses are **separate** — one document never appears in both surfaces. - -- **Incoming invoices** (`inbound_documents`) — an *external* supplier document. The **row itself is the payable**: it carries the disposition, tax treatment, event booking, re-bill linkage, supplier-payment, note, and (for re-bills) the attached customer. Categorising it **updates the document** — it never derives an `expenses` row. Mark-paid lives here. -- **Expenses** (`expenses`, `inbound_document_id IS NULL`) — *internal* own-costs: `kind = amount | mileage | per_diem` (amount = quantity × rate, rate from accounting settings with per-entry override), optional proof file, booked to an event or the company. Disposition is always `eigener_aufwand`; no supplier payment. - -This document covers the **incoming-invoices** surface. Expenses share the markup/re-bill helpers but are otherwise independent. - -## Lifecycle -``` -capture (upload / camera / email) - → inbox row, status = unsorted, parse_status = pending -triage (confirm fields + disposition + note) - ├─ eigener_aufwand → company expense (pick category), booked to company - ├─ durchlaufend → pass-through; optionally attach a client (billed at cost) - ├─ rebill → re-bill to a client (with markup) - ├─ duplikat → status = duplicate (excluded from the books) - └─ abgelehnt → status = declined (excluded from the books) -supplier payment (independent axis): markInboundSupplierPayment → supplier_paid -``` - -### Dispositions -Five: `rebill` · `durchlaufend` (Durchlaufender Posten) · `eigener_aufwand` (company expense) · `duplikat` · `abgelehnt`. - -- **`rebill`** — your own supplier cost, invoiced on to a client, usually with a **markup** (percent or flat). Requires a customer. -- **`durchlaufend`** — an amount fronted on behalf of a client and passed through **at cost / VAT-neutral**. May optionally attach a client (then it is re-billed like a rebill, but **never carries a markup** — enforced in both the UI and `categorizeInbound`). With no client it is only booked to an event/company. -- **`eigener_aufwand`** — own cost, not re-billed; pick an expense category for the Erfolgsrechnung. - -The triage modal shows an **inline explainer** for the selected disposition (`accounting.disposition.help.*`) and a **note** field on every disposition. - -### Re-categorisation -Categorising is **re-runnable** — a categorised invoice can be changed again (e.g. pass-through → company expense), including after the supplier has been paid (supplier-payment and classification are independent axes). When the document was already re-billed, `categorizeInbound` first **unwinds** the prior re-bill line (removes the invoice line, recomputes the invoice totals) before applying the new disposition. It **refuses** (`INVOICE_LOCKED`) only when the re-bill sits on an already-issued invoice — then a Storno is required (`isInvoiceMutable` mirrors the hour-entry lock rules). The only hard lock is an *issued* invoice, never supplier-payment. - -### Re-bill: cadence-aware, like hours -Re-bill/pass-through-to-a-customer consolidates onto the client's bill exactly like `customerHoursService`: - -- **Monthly / manual customers** — the line is appended **immediately** onto the customer's running monthly draft (via `invoiceService.createInvoice`'s accumulator intercept). `billed_invoice_id` is set at categorise time. -- **Per-event customers** — the item stays **PENDING** in the customer's pool (`customer_account_id` set, `billed_invoice_id` null). The inbox surfaces a **"Pending re-bills"** card grouped by customer; **"Bill these"** (`billPendingRebills`) bundles all of a customer's pending items into **one** invoice (one line per document), then navigates to the bill editor so the admin can add more lines before sending. This mirrors `billUnbilledEntries`. - -Markup resolution (rebill only): expense/document override → contract `Spesen-Zuschlag` clause → 0% (`resolveMarkup`). The re-bill line description is `"{supplier} (Weiterverrechnung)"` / `"… (Durchlaufende Position)"`. - -## Data model (migrations 122–132) -All money is integer minor units (`*_amount_minor`). Additive, hasTable/hasColumn-guarded. - -- **122** — seed `accounting` master flag (default OFF; preserve-visuals auto-enable where `taxReport` was on). -- **123** — `accounting.view` / `accounting.manage` permissions. -- **124** — `inbound_documents`, `expenses`, `expense_categories` (+ seed categories). -- **125** — contract `expense_markup_type|_percent|_flat_minor` (Spesen-Zuschlag clause). -- **126** — split incoming vs expenses: disposition/tax_treatment/event_id/category_id, re-bill markup + `billed_invoice_id`/`billed_invoice_line_item_id`, supplier-payment columns on `inbound_documents`; `kind`/`quantity`/`rate_minor` on `expenses`. -- **127** — separate `expenses` sub-flag + accounting `app_settings` (km/per-diem rate, require-proof). *(NB: `app_settings` has no `created_at/updated_at` — seed `setting_key/value/type` only.)* -- **128** — incoming mail (IMAP): `incomingMail` flag + `email_configs.imap_*` + `received_emails`. -- **129** — `ledger_accounts` + `vat_codes` (Swiss/LI KMU seed) + category→account mapping. -- **130** — `vat_code` snapshot column on quotes + invoices. -- **132** — `inbound_documents.note` + `inbound_documents.customer_account_id` (the attached re-bill client; loose link, indexed for the pending-pool lookup). - -`inbound_documents` key columns: parsed fields (`supplier_name`, `invoice_date`, `total/net/vat_amount_minor`, `iban`, `payment_reference`) + separate untrusted `qr_amount_minor` (tamper cross-check — the authoritative total is the text value); `status` (unsorted/categorized/declined/duplicate); `disposition`; `tax_treatment`; `event_id` (NULL = company); `category_id`; `customer_account_id`; `markup_type/_percent/_flat_minor`; `billed_invoice_id` + `_line_item_id`; `supplier_paid` + `_at/_method/_ref`; `note`. - -## API (`/api/admin/expenses`, gated by `incomingInvoices` + `accounting.*`) -- `POST /inbound` (multipart) — capture (upload/camera). Deduped by SHA-256. -- `GET /inbound` — list (joins the attached customer name/email). -- `GET /inbound/pending-summary` — per-customer pending re-bills (registered before `/inbound/:id`). -- `POST /inbound/bill-pending` — bundle one customer's pending re-bills into one invoice. -- `GET /inbound/:id` · `PATCH /inbound/:id` (edit/confirm fields incl. `note`). -- `GET /inbound/:id/page/:n` — rasterised PNG of a page. `GET /inbound/:id/file` — original (PDFs as attachment only, never inline). -- `POST /inbound/:id/categorize` — set disposition (re-runnable; unwinds prior re-bill). -- `POST /inbound/:id/rebill` — explicit "re-bill this one now" (forces an immediate single-doc bill). -- `POST /inbound/:id/supplier-payment` — toggle supplier paid + method/date/reference. -- Expenses: `GET/POST /`, `GET/PATCH /:id`, `POST /:id/invoice`, `POST /:id/paid`, `GET /:id/proof`. -- Categories: `GET/POST/PATCH/DELETE /categories` (accounting master). - -## Document preview = server-side rasterised images -Raw PDFs are **never** served inline. `rasterizeService` shells out to poppler `pdftoppm` (OS package in the Docker image — not a Node PDF lib, runs no JS, no egress). Pages cached under `storage/business-docs/inbound/rendered//page-.png`, served with `Content-Security-Policy: default-src 'none'` + `nosniff`. Page count capped at 200. The triage preview defaults to the last page (the Swiss QR-bill usually sits at the bottom). - -## Reporting & export -- **Tax report** (`taxReportService`) — full Einnahmen-Ausgaben: incoming invoices + expenses feed the `costs` side, grouped Company vs Event; re-billed costs are kept (the matching re-bill revenue is also counted, so it nets). `vatPayable` = output VAT − reclaimable input VAT (excludes `foreign_vat_non_reclaimable`); zero when not VAT-registered. Gated on `accounting` + `taxReport` (no longer `bills`). -- **Treuhänder export** (`ledgerService`) — accrual Buchungssätze → generic/Banana/bexio CSV. Accrual basis only; bank/payment postings are Layer B (deferred). See `project_banana_treuhaender_export_format`. -- VAT config (codes, rate→code + treatment→code maps, registration & reclaim countries, chart of accounts) lives under **Settings → Accounting**; invoices snapshot the chosen `vat_code`. - -## Flag model -`accounting` is an explicit top-level **master** flag with sub-toggles: `incomingInvoices` (this surface), `expenses` (internal expenses), `taxReport` (moved permanently out of CRM, now independent of `bills`). `incomingMail` (IMAP) is a separate flag, not under accounting. `accounting` off forces `taxReport` + `incomingInvoices` off. - -## Conventions followed -Idempotent migrations; new flags default OFF; flag reads tolerate `true|1|'1'`; money as integer `*_minor`; `requirePermission` guards; camelCase API ↔ snake_case service; multer + `safePath` containment at every file boundary; localized dates via `useLocalizedDate`; money via `utils/money`; every tax/legal surface carries a "verify with your Treuhänder" disclaimer. - -## Deferred -- **OCR / auto-extract** — `extractionService` is a no-op stub (Tesseract + Swiss-QR decode); admin reads the slip and types the fields. -- **Capture-time VAT reclaim default** — `accounting_vat_reclaim_countries` is stored but not yet consumed; needs a `supplier_country` column to default `tax_treatment`. -- **Bank reconciliation** — match incoming payments to open invoices / confirm supplier invoices paid (LLB DataFeed / camt.053 / EBICS). Phased, Swiss/LI rails. -- **Native double-entry (Layer B)** — picpeak stays a feeder/export tool below the CHF 500k threshold; full Erfolgsrechnung/Bilanz is out of scope. diff --git a/docs/backup-restore.md b/docs/backup-restore.md deleted file mode 100644 index f659f1b1..00000000 --- a/docs/backup-restore.md +++ /dev/null @@ -1,350 +0,0 @@ ---- -title: Backup & Restore -description: How picpeak captures your install, where backups land, and how to recover from them — including full disaster recovery. -sidebar_position: 2 ---- - -# Backup & Restore - -picpeak's backup system captures your entire install — database, photos, CRM documents, gallery archives, and configuration — to a destination of your choice. Recovery happens through one of two paths depending on how badly things went wrong: - -- **The install is alive** → use the **Restore wizard** in the admin UI to roll back to a chosen backup. -- **The install is gone** (host migration, `docker compose down -v`, drive replacement) → use the **install-from-backup** trigger file convention to rebuild in one boot, with no onboarding wizard and no temporary admin step. - -This guide covers both. - -## Table of contents - -- [What gets backed up](#what-gets-backed-up) -- [Destinations](#destinations) -- [Inline DB dump](#inline-db-dump) -- [Custom backup paths](#custom-backup-paths) -- [The Coverage tab](#the-coverage-tab) -- [The Integrity tab](#the-integrity-tab) -- [Restoring on a live install](#restoring-on-a-live-install) -- [Disaster recovery (install from a backup)](#disaster-recovery-install-from-a-backup) -- [Backup History detail](#backup-history-detail) -- [Settings reference](#settings-reference) -- [Troubleshooting](#troubleshooting) - -## What gets backed up - -Every "Run Backup Now" (manual or scheduled) produces: - -1. **A database dump** captured inline at the start of the run. Always included by default. picpeak refuses to ship a backup without a database dump unless the operator has explicitly opted out via the `backup_database_inline_dump` setting — see [Inline DB dump](#inline-db-dump) below. - -2. **Files from a configurable list of paths**, declared in the `backup_paths` table: - | Path | Default | Notes | - | --- | --- | --- | - | `events/active` | ✓ | Live gallery photo originals | - | `events/archived` | gated by `backup_include_archived` | Long-term archive | - | `thumbnails` | ✓ | Generated thumbnails | - | `previews` | ✓ | Lightbox preview tier | - | `heroes` | ✓ | Gallery hero images | - | `uploads` | ✓ | Wet-signature contracts, imported invoices, etc. | - | `business-docs` | ✓ | CRM PDFs, signature artefacts, imported historical invoices | - - Admins can add or remove rows from `backup_paths` to teach the walker about new feature directories — see [Custom backup paths](#custom-backup-paths). - -3. **A manifest JSON** describing the run, written to `/manifests/backup-manifest-.json`. The manifest carries the database dump path, the file inventory, checksums, and per-path counters. - -## Destinations - -picpeak supports three destination types, configured via **Backup → Configuration**: - -- **Local** — files copied to a directory on the same host (default: `/backup` inside the container, which is typically a bind mount). -- **S3 / MinIO** — files uploaded via the S3 API. Supports custom endpoints (for MinIO, Backblaze B2, Wasabi, etc.). -- **rsync** — synchronised to a remote host over SSH. - -Direct download from the admin UI is supported for local destinations; S3 backups can be retrieved via pre-signed URLs. - -## Inline DB dump - -Every "Run Backup Now" runs `pg_dump` (or `sqlite3 .backup`) inline before walking files. This guarantees the manifest's `database.backup_file` is always a fresh capture, never a stale reference to a previously-scheduled dump that may not exist. - -If the inline dump fails (disk full, pg_dump crash, permission error), the run aborts and writes the error to `backup_runs.error_message`. The UI surfaces this as a failed run — no more silent files-only manifests. - -**To opt out** (e.g. if you have a separately-orchestrated DB backup that you trust more): - -```sql -INSERT INTO app_settings (setting_key, setting_value, setting_type, updated_at) -VALUES ('backup_database_inline_dump', 'false', 'backup', NOW()) -ON CONFLICT (setting_key) DO UPDATE - SET setting_value = 'false', updated_at = NOW(); -``` - -With inline-dump opted out, picpeak's fail-loud guard still applies: a file backup with no recent DB dump on file (within 26 hours) will fail rather than ship a files-only manifest. - -## Custom backup paths - -To add a new directory to the backup walker (e.g. you've shipped a feature that drops artefacts under `storage/my-feature/`): - -```sql -INSERT INTO backup_paths (path, include_in_default, display_order, description, created_at, updated_at) -VALUES ('my-feature', true, 100, 'My new feature artefacts', NOW(), NOW()); -``` - -Next "Run Backup Now" picks it up — no restart, no migration. Set `include_in_default = false` to temporarily disable a path without dropping the row. - -The `feature_flag` column gates a path behind an app_settings boolean (matches how `events/archived` is gated by `backup_include_archived`). Useful when a backup path corresponds to an optional feature. - -## The Coverage tab - -**Backup → Coverage** answers "what will the next backup actually include?" without having to run it: - -- **Database** — inline-dump mode + last dump timestamp + staleness check -- **Configured paths** — one row per `backup_paths` entry with its current coverage status (`will-scan` / `skipped-by-toggle` / `skipped-by-feature-flag` / `missing-on-disk`) -- **Drift detection** — flags top-level directories under `STORAGE_PATH` that exist on disk but have NO matching `backup_paths` row. This is the canary for "a feature shipped without a matching backup row" — the most common cause of silent data loss in pre-2026-05 picpeak. - -The Coverage tab auto-fetches on open. If everything is green, your next backup will capture what you'd expect. - -## The Integrity tab - -**Backup → Integrity** verifies that every `*_path` column on quotes / contracts / invoices / signatures actually resolves to a file on disk, AND that files with a stored `*_sha256` still hash to the same value. Read-only, on-demand. Useful for: - -- Post-restore validation -- Detecting bit-rot -- Auditing legal-evidence artefacts before a tax review or dispute - -## Restoring on a live install - -Use this when picpeak is running and you want to roll back to a specific backup point — e.g. recovering accidentally-deleted records, reverting a bad migration, or testing a restore drill. - -**Backup → Restore** walks you through: - -1. **Source** — Local, S3, or rsync -2. **Choose Backup** — manifests discovered from disk (works after a fresh install where `backup_runs` is empty) or from the database history -3. **Restore Options** — Full / Database only / Files only / Selective + Force + Skip Pre-Restore -4. **Review** — surfaces validation warnings before you commit -5. **Restore Progress** — real-time stream of the actual steps - -Failures during restore trigger an automatic rollback from the pre-restore safety snapshot. The destination ends up either as the restored state OR as the original pre-restore state — never as a half-clobbered mix. - -### Restoring an older backup on a newer image - -picpeak's restore path is forward-compatible: a backup taken on an older version restores cleanly onto a newer image without any manual schema work. After loading the dump, the restore service runs the same `npm run migrate:safe` script that `wait-for-db.sh` uses on every container boot. Any migrations that have been added between the backup's snapshot and the current image are applied inline, against the freshly-restored DB, before the restore is reported as complete. - -Net effect: even if `bugfix/cool-new-feature` shipped a migration that adds a `widgets` table and your backup predates that branch, after restore your install has the `widgets` table (empty), the right indexes, and any seed rows the migration emits. No "you'll need to restart the container once" footnote. - -The same applies to the install-from-backup trigger — migrations land inside the restore boundary, so the moment the server prints `Server running on port 3000`, the schema matches the running image. Log in and use the install immediately. - -## Disaster recovery (install from a backup) - -For full DR after `docker compose down -v`, host migration, drive replacement, or moving an install between hosts. picpeak detects a trigger file on first boot and runs the restore before the admin UI surfaces. You open the browser, log in with your original credentials, and the install is fully populated. - -### Prerequisites - -- Your install's backup files must already be present in the `/backup` mount. They survive `docker compose down -v` because `/backup` is a bind mount, not a Docker-managed volume. -- The image must include the install-from-backup feature (shipped 2026-05-31 on `beta`; available in `main` after the next stable release). -- The backup must contain a database dump. The wizard cannot reconstruct your CRM data, customers, quotes, invoices, or admin users from a files-only backup. Confirm by inspecting any `backup-manifest-*.json` and checking that `database.backup_file` is non-null. - -### How the trigger works - -On every container start, picpeak's boot sequence checks for a trigger file in the root of the `/backup` mount. If found AND the destination database is empty, the restore runs automatically. After a successful restore the trigger file is deleted so the next boot doesn't redo the work. On failure the trigger file is preserved — fix the input and restart the container to retry. - -The trigger file is named **`RESTORE_ON_INSTALL`** (no extension) or **`RESTORE_ON_INSTALL.txt`** — either is accepted. - -### Two trigger flavors - -#### Auto-pick the newest backup - -Create an empty trigger file: - -```sh -touch /path/to/backup/RESTORE_ON_INSTALL -``` - -The boot hook will scan `/backup/manifests/` for files matching `backup-manifest-*.json` (or `.yaml`) and pick the one with the most recent modification time. Best for the common DR case where you simply want the latest snapshot. - -#### Use a specific backup - -Write the path of the manifest you want — either relative to the `/backup` mount root or an absolute path — into the trigger file: - -```sh -# Relative path (recommended) -echo "manifests/backup-manifest-backup-20260530-190617-e9be97b3.json" \ - > /path/to/backup/RESTORE_ON_INSTALL - -# Or absolute path inside the container -echo "/backup/manifests/backup-manifest-backup-20260530-190617-e9be97b3.json" \ - > /path/to/backup/RESTORE_ON_INSTALL -``` - -Use this when you need to restore an older backup (e.g. rolling back a data corruption that happened after the most recent backup ran). - -### Full DR walkthrough - -```sh -# 1. Snapshot the backup outside the compose directory (belt + suspenders). -# This is a docker-compose-down-v-proof copy in case anything goes wrong. -SNAPSHOT=~/picpeak-snapshots/$(date +%Y%m%d-%H%M%S) -mkdir -p "$SNAPSHOT" && cp -av /path/to/picpeak/backup/. "$SNAPSHOT/" - -# 2. Verify the backup is restorable. database.backup_file must be non-null. -LATEST=$(ls -t /path/to/picpeak/backup/manifests/*.json | head -1) -docker compose exec backend cat "/backup/manifests/$(basename $LATEST)" \ - | python3 -c "import json,sys; m=json.load(sys.stdin); \ - print('DB included:', bool(m.get('database',{}).get('backup_file')))" - -# 3. Drop the trigger file. Two variants — pick one: - -# (a) auto-pick newest -touch /path/to/picpeak/backup/RESTORE_ON_INSTALL - -# (b) specific manifest -echo "manifests/backup-manifest-backup-20260530-190617-e9be97b3.json" \ - > /path/to/picpeak/backup/RESTORE_ON_INSTALL - -# 4. Boot. -docker compose down -v -docker compose up -d -docker compose logs -f backend --tail=100 -``` - -When the boot log shows `Install-from-backup: restore completed successfully` followed by `Server running on port 3000`, the install is ready. Open the admin UI and log in with your original (pre-disaster) credentials. - -### Safety gates - -Three layers prevent accidental data loss: - -1. **The trigger file must exist.** No auto-magic — an admin explicitly drops the file to signal intent. - -2. **The destination database must be empty.** If the database contains any events, the install-from-backup hook refuses to run. The fresh-install default admin (auto-created by migration 001) is treated as throwaway and replaced by the backup's admin row, so a single admin user does not block the restore. - -3. **Failed restores roll back to the pre-restore state.** If anything fails after the DROP DATABASE step, picpeak's automatic rollback restores the destination from the pre-restore safety backup it took before starting. - -#### Override for advanced cases - -If you have a populated install you intentionally want to clobber (dev rebuilds, staging refresh, etc.): - -```yaml -# In docker-compose.yml -backend: - environment: - - INSTALL_FROM_BACKUP_FORCE=true -``` - -Or via the CLI: - -```sh -INSTALL_FROM_BACKUP_FORCE=true docker compose up -d backend -``` - -With this set, gate #2 is skipped and the restore proceeds even with existing data. Gates #1 (trigger file presence) and #3 (rollback on failure) still apply. - -### Verifying DR success - -After the boot log shows `Install-from-backup: restore completed successfully`: - -```sh -# Trigger should be gone (deleted on successful restore) -ls /path/to/picpeak/backup/RESTORE_ON_INSTALL 2>/dev/null \ - || echo "Trigger cleaned up — restore succeeded." - -# Inspect the restore_runs row -docker compose exec -T postgres psql -U picpeak -d picpeak_prod -c \ - "SELECT id, status, was_successful, was_rollback_attempted FROM restore_runs ORDER BY id DESC LIMIT 1;" - -# Confirm data is back -docker compose exec -T postgres psql -U picpeak -d picpeak_prod -c " - SELECT 'admin' AS t, COUNT(*) FROM admin_users - UNION ALL SELECT 'events', COUNT(*) FROM events - UNION ALL SELECT 'invoices', COUNT(*) FROM invoices - UNION ALL SELECT 'app_settings', COUNT(*) FROM app_settings;" -``` - -Then open the admin login and use your **original** pre-disaster credentials. - -## Backup History detail - -Each row in **Backup → Backup History** expands to show: - -- **Database** — whether the dump was included -- **Per-path file counts** — one row per `backup_paths` entry that contributed files, with count + total size. e.g.: - ``` - events/active 142 (3.2 GB) - business-docs 17 (4.5 MB) - thumbnails 142 (12.4 MB) - ``` -- **Total files** + total bytes -- Error message if the run failed - -This breakdown reflects Stage B's data-driven walker, so admins can see at a glance which paths contributed how much. - -## Settings reference - -Backup-related settings live in `app_settings` with `setting_type = 'backup'` or `setting_type = 'restore'`: - -| Setting | Default | Notes | -| --- | --- | --- | -| `backup_enabled` | true | Master scheduler switch | -| `backup_destination_type` | `'local'` | `'local'`, `'s3'`, or `'rsync'` | -| `backup_destination_path` | `'/backup'` | Local destination | -| `backup_database_inline_dump` | true | Inline DB dump on every run | -| `backup_include_archived` | false | Gate `events/archived` | -| `backup_incremental` | true | Skip unchanged files (checksum-tracked) | -| `restore_allow_force` | true | Permit Force Restore via the wizard | -| `restore_require_pre_backup` | true | Take a pre-restore safety snapshot | -| `restore_verify_checksums` | true | Verify file checksums after restore | -| `restore_email_on_completion` | true | Notify admin when restore finishes | -| `restore_retention_days` | 30 | How long pre-restore snapshots survive | - -Most settings are exposed in the **Backup → Configuration** tab. Less common ones can be set via SQL. - -## Troubleshooting - -### "Run Backup Now" fails with `No database backup available` - -The inline DB dump was disabled AND no recent scheduled dump exists. Either re-enable inline dumps (set `backup_database_inline_dump = 'true'`) or configure a scheduled DB dump that completes within the staleness window (default 26h). - -### Backup History row says "completed" but shows 0 files - -This is the legacy of a pre-2026-05 install where the walker was hard-coded and missed paths. After upgrading, the new walker captures everything per `backup_paths`. The 0-file row is historical — new backups will count correctly. - -### Restore wizard shows "No backups found" - -The wizard's disk discovery looks in `backup_destination_path` + its `manifests/` subdirectory. If you moved manifests elsewhere or your bind mount points at a different host directory than expected, the discovery won't find them. Check `backup_destination_path` in the Configuration tab matches reality. - -### Restore completes but login fails - -Caused by the pre-2026-05 dead-pool bug — fixed in the current image. If you're on a stale image and still see this, restart the backend container once: - -```sh -docker compose restart backend -``` - -Then try logging in again. - -### Install-from-backup: boot log shows no `Install-from-backup:` lines - -Check that the trigger file is actually visible from inside the container: - -```sh -docker compose exec backend ls -la /backup/RESTORE_ON_INSTALL -docker compose exec backend cat /backup/RESTORE_ON_INSTALL -``` - -If `ls` reports the file but the hook didn't run, the most likely cause is that the trigger pointed at a manifest that doesn't exist. The hook silently returns when the manifest path can't be resolved. Verify the path inside the file matches an actual manifest: - -```sh -docker compose exec backend ls -la /backup/manifests/ -``` - -### Install-from-backup: restore fails and leaves the trigger file in place - -This is the intentional behavior — fix the input, then restart the container to retry. The boot log will surface the underlying error (e.g. corrupt manifest, missing database dump file, validator warnings without the force override). - -Common failure modes: - -- **Backup is files-only** (`database.backup_file = null` in the manifest). Pick a different backup or proceed with caution via the admin wizard, knowing you will restore files only. -- **Manifest path mismatch.** The path in the trigger file points at a manifest that doesn't exist. Either fix the path or use the empty-file auto-pick variant. -- **Existing data** without the force override. Either start from a truly empty install (`docker compose down -v`) or set `INSTALL_FROM_BACKUP_FORCE=true`. - -### How do I disable install-from-backup entirely? - -Don't create a `RESTORE_ON_INSTALL` file. Without the trigger, the hook is a no-op on every boot. There is no separate "off switch" because the feature is opt-in by design. - -## See also - -- [Deployment](/deployment) — Docker, environment variables, volumes -- [Admin Settings](/guides/admin-settings) — Configuration tab walkthrough diff --git a/docs/crm-disclaimers.md b/docs/crm-disclaimers.md deleted file mode 100644 index eb953e59..00000000 --- a/docs/crm-disclaimers.md +++ /dev/null @@ -1,164 +0,0 @@ -# CRM disclaimers — important reading for every picpeak operator - -The CRM module ships defaults that touch two regulated areas: **legally -binding contracts** and **payment instruments (QR-bills, IBAN/BIC)**. -The text and data picpeak renders are mechanically correct, but the -**substance is the operator's responsibility**. - -> [!IMPORTANT] -> Whatever picpeak ships in these two areas is an **EXAMPLE ONLY**. It is -> every operator's own duty to have the content reviewed by their lawyer -> (for contracts) and verified with their bank (for QR-bills / SEPA EPC -> payloads) before sending it to a customer. Picpeak does not provide -> legal advice and cannot validate banking data — only the operator can. - -## 1. Contract block library - -The contract feature (`feat/crm`, migration `130_add_contracts.js`) seeds -twelve "system" blocks across six sections: - -- Basics — contract subject / scope-of-work header -- Scope — image-rights clauses (private + commercial variants) -- Privacy — model-release clauses (private / commercial / minors), DSGVO notice -- Commercial — payment-terms reference, tiered cancellation schedule -- NDA — mutual confidentiality -- Closing — jurisdiction (CH + DE variants) - -All bodies are hand-written by the picpeak maintainer (DE first, EN -translated). **None of them have been reviewed by a lawyer.** They are -intended as starting points — every operator must: - -1. Read each system block they intend to send. -2. Adjust the body text to match their own jurisdiction, business - structure, and risk profile, in consultation with their lawyer. -3. Where appropriate, replace a system block entirely with admin-authored - blocks under their lawyer's guidance. - -The admin UI surfaces this disclaimer: -- as a persistent banner on the Block Library page, -- as a persistent banner on the Contract Editor, -- as a "system block" badge plus an "Examples only — have your lawyer - review" line on every seeded block's description. - -System blocks **cannot be deleted** (the seed migration would re-create -them on re-run); operators who reject a seeded block toggle -`is_active=false` on it so it stops appearing in new contracts. The body -text of a system block is fully editable — when an operator's lawyer -delivers a reviewed version, the operator pastes it into the system -block and the new body is what gets snapshotted onto every subsequent -contract. - -## 2. QR-bill / SEPA EPC payment payloads - -Picpeak is an open-source project. The invoice feature renders Swiss -QR-bills and SEPA EPC QR codes from the data you typed (IBAN, BIC, -account holder, amount, reference) — that's it. We don't have a way to -tell whether the code actually scans correctly in your bank's app, so -**please test that yourself before sending real invoices**. - -Before going live: - -1. Print one test invoice with the QR code. -2. Scan it with the e-banking app of your own bank. -3. If you expect customers on other banks (UBS, PostFinance, Raiffeisen, - Migros Bank for Swiss QR; any major SEPA bank for EPC QR), scan with - those too. -4. If it doesn't scan or the prefilled fields look wrong, fix your - bank-account data in picpeak and try again. - -**We are not responsible for any mistakes** in the rendered QR codes, -payment data, or anything that flows from sending an invoice with bad -data on it. That's why picpeak is MIT-licensed — use it freely, but -the verification is on you. - -The admin UI surfaces this same note as a banner on the Business -Profile → Bank Accounts and QR-format settings pages. - -## 3. Signature type — picpeak provides SES, not QES - -The contract signing flow (typed name + acceptance checkbox + canvas -signature image + IP address + timestamp + SHA-256 audit page) is a -**Simple Electronic Signature (SES)** under the EU eIDAS regulation -and the Swiss ZertES. SES is the same legal tier as DocuSign's basic -plan, HelloSign's free tier, or Adobe Acrobat Sign without a -qualified-certificate add-on. - -### What SES is legally sufficient for - -In DACH (CH, DE, AT, FL), SES is valid and routinely upheld in civil -court for contracts that **don't** legally require a specific form: - -- Photography service agreements -- Image-rights / model-release clauses -- Cancellation policies -- NDAs between private parties -- Most commercial service contracts -- Most B2B agreements - -For these, picpeak's evidence chain (frozen block bodies + signature -images + names + IPs + timestamps + content hashes + immutable -audit-log timeline) is comparable to what an SES provider charging -€10–30/month delivers. The audit page appended to the signed PDF -makes the evidence self-contained — the customer can re-hash their -copy and prove integrity without trusting picpeak's database. - -### What SES is NOT sufficient for - -Certain documents **legally require Schriftform** (handwritten -signature on paper) OR a **Qualified Electronic Signature (QES)** -backed by a certificate from an accredited Trust Service Provider -(Swisscom Sign, D-Trust, A-Trust, Bundesdruckerei, etc.). The most -common categories in DACH: - -| Jurisdiction | Document type | Statute | -|---|---|---| -| DE | Bürgschaft (guaranty) | § 766 BGB | -| DE | Verbraucherdarlehensvertrag (consumer loan) | § 492 BGB | -| DE | Befristete Arbeitsverträge (fixed-term employment) | § 14 Abs. 4 TzBfG | -| DE | Kündigung Arbeitsverhältnis (employment termination) | § 623 BGB | -| DE | Aufhebungsvertrag (employment cancellation agreement) | § 623 BGB | -| CH | Bürgschaft above CHF 2'000 | Art. 493 OR | -| CH | Eheverträge (matrimonial property agreements) | Art. 184 ZGB | -| AT | Bürgschaftserklärung (guaranty declaration) | § 1346 ABGB | - -If you send any of these via picpeak's signing flow, the signature -is **legally invalid** and the contract may be unenforceable. Use a -QES provider for these documents. - -If you're unsure which category your contract falls into, ask your -lawyer. The cost of asking is hours; the cost of getting it wrong -is years. - -### What picpeak does NOT provide - -- **Identity verification.** Anyone who receives the signing email - can sign. There's no second factor (SMS, video ident, ID upload). -- **Qualified-certificate-based signatures (QES).** Requires a - separate service. -- **External / third-party timestamp.** All timestamps are - server-side; an RFC 3161 Trust Service Provider timestamp would - close the clock-manipulation defence but isn't currently part of - the audit page. -- **WORM / immutable storage.** Signed PDFs live on the regular - filesystem path under `storage/business-docs/contract//`. - Hardening this for high-stakes contracts is an infrastructure- - level decision (S3 Object Lock, etc.) outside picpeak's code. - -The signing flow is fine for routine photographer-customer -contracts. For anything with significant economic value or -Schriftform-bound documents, layer a QES provider on top of -picpeak's contract management. - -## Why this matters - -- **Liability.** Sending an unreviewed contract or a malformed QR-bill - is the operator's liability — not picpeak's. The MIT licence - explicitly disclaims warranty. -- **Jurisdictional variance.** Even the most carefully drafted clause - is wrong somewhere. The CH-jurisdiction closing block won't help a - photographer in Bavaria. The cancellation schedule that's standard - in Zurich would be challenged in Berlin. - -If you are unsure: don't send. Pause, read this file again, and run -your seeded contract content past your lawyer (or scan one test QR-bill -yourself) before turning the feature on for live customers. diff --git a/docs/fonts.md b/docs/fonts.md deleted file mode 100644 index 2bfca412..00000000 --- a/docs/fonts.md +++ /dev/null @@ -1,106 +0,0 @@ -# Self-hosted webfonts - -PicPeak ships with a curated set of webfonts baked into the backend image and serves them from your own origin. **No requests go to `fonts.googleapis.com` or any third-party CDN** — guest IPs stay private, which is important for GDPR compliance (LG München 2022). - -The font picker in the admin theme customizer is **data-driven**: whatever the backend finds on disk, the picker offers. This page documents the conventions and the workflow for adding your own families. - -## What ships out of the box - -The Docker image bundles 8 OFL-licensed families at `backend/assets/fonts/`: - -- Comic Neue -- IBM Plex Sans -- Inter (the default) -- Jost -- Montserrat -- Noto Sans -- Playfair Display -- Poppins - -These appear in the admin theme customizer with no configuration. - -## Adding your own font (drop a folder, restart) - -You don't need to fork the repo. Place a font folder in your runtime storage volume — the same volume that holds events, thumbnails, etc. — and it appears in the picker after the next backend restart (or within ~30 seconds of being added, whichever comes first). - -### 1. Choose where on the host - -Bind-mount target inside the container is `/app/storage/fonts/` (the env var `STORAGE_PATH` controls the prefix; defaults to `/app/storage`). On the host, that's wherever your `docker-compose.yml` mounts `${APP_STORAGE}` from — typically `./storage/`. - -### 2. Folder layout - -``` -storage/fonts/ -└── / - ├── 400.woff2 - ├── 600.woff2 - ├── 700.woff2 - └── meta.json (optional) -``` - -Rules: - -- **Folder name** = display family name with spaces replaced by hyphens. The scanner turns `Roboto-Slab/` → `Roboto Slab`. Use the exact upstream family name; capitalisation is preserved. -- **File names** are `.woff2` where `` is an integer (100-900). Other names are ignored. The picker doesn't expose individual weights, but the runtime injects all available weights in the `@font-face` block so headings (semibold/bold) render correctly. -- **Format** must be `.woff2`. Other formats are ignored. WOFF2 is universally supported and the smallest on the wire. -- **No italics** in v1 (the picker doesn't expose them). Italic files in the folder are silently ignored. -- **`meta.json`** (optional) tells the picker which CSS generic family to fall back to while the font file is loading (and permanently if the file ever 404s). Shape: `{ "generic": "sans-serif" | "serif" | "cursive" | "monospace" }`. Defaults to `sans-serif` if absent. Add this for serif fonts (e.g. Playfair Display) and cursive/display fonts (e.g. Comic Neue, Lobster) so visitors don't briefly see Helvetica during the font fetch. - -### 3. Where to download fonts - -For Google-Fonts-licensed families, use [google-webfonts-helper](https://gwfh.mranftl.com/fonts): - -1. Pick the family. -2. Charsets section → **Latin** only (uncheck others unless you actually need them; Cyrillic alone roughly doubles file size). -3. Styles section → **400, 600, 700** at minimum (these match what the picker uses). -4. Click "Download files" — you'll get a ZIP containing the `.woff2` files plus the family's OFL license. -5. Rename the files to `400.woff2`, `600.woff2`, `700.woff2` and drop them in `storage/fonts//`. -6. Keep the OFL license file alongside (the static handler serves anything in the folder, so `/fonts//OFL.txt` is publicly available — this satisfies OFL §2's "license must be included with all copies"). - -For non-Google fonts, ensure you have the right to redistribute. SIL Open Font License (OFL), Apache 2.0, and most "free for commercial use" web licenses allow this. - -### 4. Activation - -Either: - -- **Restart the backend container** (immediate), or -- **Wait ~30 seconds** for the in-memory cache to expire and the next `/api/public/fonts` request to re-scan. - -Refresh the admin customizer; the new family appears in the body and heading dropdowns. - -> **Note:** The admin customizer caches the fonts list separately for 5 minutes (React Query staleTime). After the backend picks up a new family, hard-reload the customizer page (⌘+Shift+R / Ctrl+Shift+R) to see it immediately, or wait up to 5 minutes for the frontend cache to expire on its own. The two caches serve different purposes — the backend avoids disk hits per request; the frontend avoids network hits per re-render — so we keep them independent and document the worst case rather than try to synchronise them. - -## How it works - -- **Scanner**: `backend/src/services/fontsService.js` reads two locations and merges them: `backend/assets/fonts/` (bundled) + `STORAGE_PATH/fonts/` (user). User additions override bundled families of the same name. Cached for 30 s. -- **Listing endpoint**: `GET /api/public/fonts` returns `{ fonts: [{ family, weights, generic }, ...] }`. -- **Static serving**: `GET /fonts//.woff2` returns the actual file. Path-traversal protected. `Cache-Control: max-age=7d` — clients revalidate via `If-Modified-Since` after expiry, so replacing a file on disk eventually rolls out without admin action (see "Replacing an existing font" below). -- **Lazy injection**: `frontend/src/contexts/ThemeContext.tsx` watches `theme.fontFamily` / `theme.headingFontFamily` and injects exactly one `@font-face` block per family the page actually uses, into a single `