From 6b99f52dacedabc9dfc5e28c20a27f23bd463d6b Mon Sep 17 00:00:00 2001 From: Dave Date: Sat, 17 Oct 2026 15:34:41 +0200 Subject: [PATCH] chore(pocketbase): track live quotes hook and add git-based hook sync quotes.pb.js is copied byte-for-byte from the live WFDASH PocketBase. DEPLOY lists the hooks the VPS cron syncs into the hooks volume; the audit hooks stay undeployed for now. Co-Authored-By: Claude Opus 5.5 --- AGENTS.md | 7 ++- pocketbase/pb_hooks/DEPLOY | 8 ++++ pocketbase/pb_hooks/quotes.pb.js | 63 +++++++++++++++++++++++++++ scripts/pb-hooks/README.md | 24 +++++++++++ scripts/pb-hooks/sync-pb-hooks.sh | 72 +++++++++++++++++++++++++++++++ 5 files changed, 172 insertions(+), 2 deletions(-) create mode 100644 pocketbase/pb_hooks/DEPLOY create mode 100755 pocketbase/pb_hooks/quotes.pb.js create mode 100644 scripts/pb-hooks/README.md create mode 100755 scripts/pb-hooks/sync-pb-hooks.sh diff --git a/AGENTS.md b/AGENTS.md index 7c1a418..44394b6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -135,8 +135,11 @@ Read side: `src/lib/server/audit-log-service.ts` → Noisy fields (search/AI/timers) are skipped via `IGNORED_FIELDS` in the hook utils — add new auto-updated fields there. -The hook files are deployed by hand into the PB server's `pb_hooks/` -(like select values, it's a manual step on the live instance). +Hooks deploy from git: a VPS cron syncs the files listed in +`pocketbase/pb_hooks/DEPLOY` from `main` into the live PB hooks volume +(see `scripts/pb-hooks/README.md`). **As of 2026-10-17 only `quotes.pb.js` +is deployed — the audit hooks are NOT live yet**, so `audit_log` is empty +until both `audit_log.pb.js` and `audit_log_utils.js` are added to `DEPLOY`. # Giellie agent (`/giellie`) — tools come from the MCP, policy lives in one file diff --git a/pocketbase/pb_hooks/DEPLOY b/pocketbase/pb_hooks/DEPLOY new file mode 100644 index 0000000..ca2b207 --- /dev/null +++ b/pocketbase/pb_hooks/DEPLOY @@ -0,0 +1,8 @@ +# Hook files the VPS sync copies into the live WFDASH PocketBase. +# One filename per line (relative to this folder). Anything not listed here +# is NOT deployed, and a *.js file on the server that is not listed here is +# removed on the next sync. See scripts/pb-hooks/README.md. +# +# Not deployed yet: audit_log.pb.js + audit_log_utils.js — add BOTH lines +# together when the audit trail is switched on (utils is required by the hook). +quotes.pb.js diff --git a/pocketbase/pb_hooks/quotes.pb.js b/pocketbase/pb_hooks/quotes.pb.js new file mode 100755 index 0000000..071f70b --- /dev/null +++ b/pocketbase/pb_hooks/quotes.pb.js @@ -0,0 +1,63 @@ +onRecordCreate((e) => { + if (e.record.getString("quote_number") !== "") { + e.next(); + return; + } + + try { + var now = new Date(); + var year = now.getFullYear(); + var month = String(now.getMonth() + 1).padStart(2, '0'); + var prefix = "Q" + year + month; + + let attempts = 0; + let candidate; + + do { + // findRecordsByFilter accepts sort as the 3rd arg. + // findFirstRecordByFilter's 3rd arg is params (dbx.Params), + // which is why the old "-quote_number" sort string threw + // "could not convert -quote_number to dbx.Params". + const records = $app.findRecordsByFilter( + "quotes", + "quote_number != ''", + "-quote_number", + 1, + 0 + ); + + let newNum = 1; + if (records && records.length > 0) { + var raw = records[0].getString("quote_number"); + // Extract trailing digits (handles Q202607011, + // legacy Q-2026-07-011, Q1782862717620, etc.). + var m = raw.match(/(\d+)$/); + if (m) { + newNum = parseInt(m[1], 10) + 1 + attempts; + } + } + + candidate = prefix + String(newNum).padStart(3, '0'); + attempts++; + + // check if this candidate already exists + var exists; + try { + exists = $app.findFirstRecordByFilter( + "quotes", + "quote_number = {:num}", + { num: candidate } + ); + } catch (e) { + exists = null; // not found throws in some versions + } + } while (exists && attempts < 5); + + e.record.set("quote_number", candidate); + } catch (err) { + console.error("Auto-numbering error:", err); + e.record.set("quote_number", "Q" + Date.now()); // fallback that's at least unique + } + + e.next(); +}, "quotes"); diff --git a/scripts/pb-hooks/README.md b/scripts/pb-hooks/README.md new file mode 100644 index 0000000..586b118 --- /dev/null +++ b/scripts/pb-hooks/README.md @@ -0,0 +1,24 @@ +# PocketBase hooks sync (WFDASH) + +The live WFDASH PocketBase (Coolify service `pocketbase-wfdash`, container +`pocketbase-gnqc88suy8fqk6wyryjjdpvl`, PocketBase 0.39.8) loads its hooks +from the Docker volume `gnqc88suy8fqk6wyryjjdpvl_pocketbase-hooks`, mounted +at `/app/pb_hooks`. + +Hooks are deployed from git, not edited on the server: + +1. Edit or add files in `pocketbase/pb_hooks/` in this repo. +2. List every file to deploy in `pocketbase/pb_hooks/DEPLOY`. +3. Push to `main` on Forgejo. + +Within ~2 minutes the VPS cron runs `sync-pb-hooks.sh`, which pulls `main` +into `/opt/wfdash-pb-hooks/repo` (sparse clone, read-only deploy key) and +copies the listed files into the volume. PocketBase reloads itself when a hook +file changes. A `.js` file on the server that isn't in `DEPLOY` is removed. + +- Log: `/var/log/wfdash-pb-hooks-sync.log` on the VPS. +- Force a sync: `/opt/wfdash-pb-hooks/repo/scripts/pb-hooks/sync-pb-hooks.sh --force` +- Roll back: revert the commit (or remove the line from `DEPLOY`) and push. +- Pause syncing: comment out the cron line (`crontab -e` as root). + +The sync never touches `pb_data` (the database). diff --git a/scripts/pb-hooks/sync-pb-hooks.sh b/scripts/pb-hooks/sync-pb-hooks.sh new file mode 100755 index 0000000..e8932f4 --- /dev/null +++ b/scripts/pb-hooks/sync-pb-hooks.sh @@ -0,0 +1,72 @@ +#!/bin/sh +# Sync the WFDASH PocketBase hooks from git into the live hooks volume. +# +# Runs on the VPS from cron. Pulls origin/main into a sparse clone and copies +# every file listed in pocketbase/pb_hooks/DEPLOY into the PocketBase hooks +# volume. PocketBase reloads itself when a file in pb_hooks changes, so no +# container restart is needed. Files are only written when their content +# changed, so an unchanged repo never triggers a reload. +# +# Usage: sync-pb-hooks.sh [--force] (--force re-syncs even if HEAD is current) +set -eu + +REPO="${PB_HOOKS_REPO:-/opt/wfdash-pb-hooks/repo}" +TARGET="${PB_HOOKS_TARGET:-/var/lib/docker/volumes/gnqc88suy8fqk6wyryjjdpvl_pocketbase-hooks/_data}" +LOCK="${PB_HOOKS_LOCK:-/tmp/wfdash-pb-hooks-sync.lock}" + +log() { echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) $*"; } + +exec 9>"$LOCK" +if ! flock -n 9; then + log "another sync is running; skipping" + exit 0 +fi + +cd "$REPO" +git fetch --quiet origin main +if [ "$(git rev-parse HEAD)" = "$(git rev-parse origin/main)" ] && [ "${1:-}" != "--force" ]; then + exit 0 +fi +git reset --quiet --hard origin/main + +SRC="$REPO/pocketbase/pb_hooks" +LIST="$SRC/DEPLOY" +if [ ! -f "$LIST" ]; then + log "no $LIST; nothing deployed" + exit 1 +fi + +# Validate the whole list before touching the live folder. +WANTED=$(grep -v '^[[:space:]]*#' "$LIST" | sed 's/[[:space:]]*$//' | grep -v '^$' || true) +for f in $WANTED; do + case "$f" in + */*|.*) log "refusing unsafe name in DEPLOY: $f"; exit 1 ;; + esac + if [ ! -f "$SRC/$f" ]; then + log "listed file missing in repo: $f — nothing changed" + exit 1 + fi +done + +changed=0 +for f in $WANTED; do + if ! cmp -s "$SRC/$f" "$TARGET/$f"; then + cp "$SRC/$f" "$TARGET/$f.tmp" + mv "$TARGET/$f.tmp" "$TARGET/$f" + log "updated $f" + changed=1 + fi +done + +# Remove deployed .js files that are no longer listed. +for path in "$TARGET"/*.js; do + [ -e "$path" ] || continue + name=$(basename "$path") + if ! printf '%s\n' $WANTED | grep -qx "$name"; then + rm -f "$path" + log "removed $name (not in DEPLOY)" + changed=1 + fi +done + +log "synced $(git rev-parse --short HEAD) (changed=$changed)"