SPB Git

build resilience: sitemap/home tolerate an unavailable database; deploy hooks migrate before build

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Simon-Pierre Boucher committed (Nov 2, 2026) parent 19c0ff7 Browse files

4 changed files +5 −4

modified docs/deployment.md +1 −1
@@ -23,7 +23,7 @@ mkdir -p ~/apps/tools-spboucher-ai ~/backups/tools-spboucher-ai && chmod 700 ~/b
23 23 Secrets in `.env`: `DATABASE_URL` (tools_app), `DATABASE_URL_PUBLIC` (tools_reader), `DATABASE_URL_ADMIN` (owner role for migrations/backups), `SESSION_SECRET`, `WEBAUTHN_RP_ID=tools.spboucher.ai`, `WEBAUTHN_ORIGIN=https://tools.spboucher.ai`, `APP_URL`, `OWNER_EMAIL`, `OWNER_INITIAL_PASSWORD` (changed at first login), `BACKUP_PASSPHRASE`, `BACKUP_DIR`, `BACKUP_REMOTE`, `AI_PROVIDER=none`.
24 24
25 25 ## Manifest (`infra/mld/tools-spboucher-ai.json` → `M1M32:~/dispatch/apps/`)
26 −PM2 `tools-web` = `node node_modules/next/dist/bin/next start -p 8540 -H 0.0.0.0`; `tools-worker` = `node node_modules/tsx/dist/cli.mjs src/worker/index.ts`. Hooks (post_sync): require `.env` → `pnpm install --frozen-lockfile` → `pnpm build` → `pnpm db:migrate` → `pnpm content:import --publish`. Placement pinned to M2M32c. `tunnel.domain = tools.spboucher.ai`.
26 +PM2 `tools-web` = `node node_modules/next/dist/bin/next start -p 8540 -H 0.0.0.0`; `tools-worker` = `node node_modules/tsx/dist/cli.mjs src/worker/index.ts`. Hooks (post_sync): require `.env` → `pnpm install --frozen-lockfile` → `pnpm db:migrate` + `pnpm content:import --publish` + `pnpm bootstrap` → `pnpm build` (static generation reads the database, so migrations run first). Placement pinned to M2M32c. `tunnel.domain = tools.spboucher.ai`.
27 27
28 28 ## Release procedure (remote-first after first deploy)
29 29 First deploy (creates the app):
modified infra/mld/tools-spboucher-ai.json +1 −1
@@ -94,8 +94,8 @@
94 94 "post_sync": [
95 95 "set -o pipefail; export PATH=\"/opt/homebrew/bin:$PATH\"; test -f .env || { echo 'ERREUR: .env absent (secrets) — copier ~/Desktop/Cluster/secrets/tools-spboucher-ai/.env'; exit 1; }; chmod 600 .env",
96 96 "set -o pipefail; export PATH=\"/opt/homebrew/bin:$PATH\"; pnpm install --frozen-lockfile --silent && test -d node_modules/next && echo ' deps ok'",
97 − "set -o pipefail; export PATH=\"/opt/homebrew/bin:$PATH\"; set -a; . ./.env; set +a; NEXT_TELEMETRY_DISABLED=1 pnpm build 2>&1 | tail -3 && test -f .next/BUILD_ID && echo ' build ok'",
98 97 "set -o pipefail; export PATH=\"/opt/homebrew/opt/postgresql@17/bin:/opt/homebrew/bin:$PATH\"; set -a; . ./.env; set +a; pnpm db:migrate && pnpm content:import --publish > /dev/null && pnpm bootstrap && echo ' db ok'",
98 + "set -o pipefail; export PATH=\"/opt/homebrew/bin:$PATH\"; set -a; . ./.env; set +a; NEXT_TELEMETRY_DISABLED=1 pnpm build 2>&1 | tail -3 && test -f .next/BUILD_ID && echo ' build ok'",
99 99 "mkdir -p ~/backups/tools-spboucher-ai && chmod 700 ~/backups/tools-spboucher-ai && echo ' backup dir ok'"
100 100 ],
101 101 "post_start": []
modified src/app/(public)/page.tsx +1 −1
@@ -15,7 +15,7 @@ const PATHS = [
15 15 ];
16 16
17 17 export default async function Home() {
18 − const { stats, featured, recent, tree, collections } = await homeData();
18 + const { stats, featured, recent, tree, collections } = await homeData().catch(() => ({ stats: { tools: 0, deep: 0, categories: 0, vendors: 0, collections: 0, lastVerified: null }, featured: [], recent: [], tree: [], collections: [] }));
19 19 return (
20 20 <>
21 21 <section className="relative overflow-hidden border-b border-line">
modified src/app/sitemap.ts +2 −1
@@ -3,7 +3,8 @@ import { allPublicSlugs, categoryTree, listCollections } from "@/lib/public/quer
3 3 import { APP_URL } from "@/lib/env";
4 4 export const revalidate = 3600;
5 5 export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
6 − const [tools, tree, cols] = await Promise.all([allPublicSlugs(), categoryTree(), listCollections()]);
6 + // Never fail the build if the database is unavailable at generation time (revalidated hourly at runtime).
7 + const [tools, tree, cols] = await Promise.all([allPublicSlugs(), categoryTree(), listCollections()]).catch(() => [[], [], []] as const);
7 8 return [
8 9 { url: `${APP_URL}/`, changeFrequency: "daily", priority: 1 }, { url: `${APP_URL}/tools`, changeFrequency: "daily", priority: 0.9 }, { url: `${APP_URL}/categories`, changeFrequency: "weekly", priority: 0.7 }, { url: `${APP_URL}/collections`, changeFrequency: "weekly", priority: 0.7 }, { url: `${APP_URL}/about`, changeFrequency: "monthly", priority: 0.4 },
9 10 ...tree.flatMap((c) => [{ url: `${APP_URL}/categories/${c.slug}`, changeFrequency: "weekly" as const, priority: 0.6 }, ...c.children.map((x) => ({ url: `${APP_URL}/categories/${x.slug}`, changeFrequency: "weekly" as const, priority: 0.6 }))]),
10 11
Menu

Theme