diff --git a/CloudronManifest.json b/CloudronManifest.json index ea8598d..cc4b5c1 100644 --- a/CloudronManifest.json +++ b/CloudronManifest.json @@ -4,7 +4,7 @@ "title": "Community Rule", "author": "MEDLab", "description": "Community governance and rule-building app", - "version": "0.1.7", + "version": "0.1.8", "httpPort": 3000, "healthCheckPath": "/api/health", "memoryLimit": 805306368, diff --git a/Dockerfile b/Dockerfile index f0537f9..02db462 100644 --- a/Dockerfile +++ b/Dockerfile @@ -48,6 +48,9 @@ COPY --from=builder --chown=node:node /app/public ./public COPY --from=builder --chown=node:node /app/.next/standalone ./ COPY --from=builder --chown=node:node /app/.next/static ./.next/static COPY --from=builder --chown=node:node /app/prisma ./prisma +# Facet/template seed JSON — NOT under /app/data (localstorage mount overlays that). +COPY --from=builder --chown=node:node /app/data ./seed-data +ENV SEED_DATA_DIR=/app/seed-data # Prisma CLI (devDependency) is not in the Next.js standalone trace. Install # globally in the runner so start.sh can run `prisma migrate deploy`. diff --git a/docs/guides/ops-backend-deploy.md b/docs/guides/ops-backend-deploy.md index 200ee53..585c518 100644 --- a/docs/guides/ops-backend-deploy.md +++ b/docs/guides/ops-backend-deploy.md @@ -390,6 +390,16 @@ production env vars, and verify the vertical slice before apex cutover custom-from allowed — §3). 5. **Confirm the app is running** in the Cloudron dashboard (Logs tab). Look for a clean `prisma migrate deploy` and Next.js listening on port 3000. +6. **Seed facet data (one-time per environment)** — templates + `MethodFacet` + rows for create-flow "Recommended" tags are **not** applied at boot. After + first install (or when recommendations return all-zero scores), run: + ```bash + cloudron exec --app staging.communityrule.info -- \ + node prisma/seed.bundle.cjs + ``` + JSON lives at `/app/seed-data/` (`SEED_DATA_DIR`); do not use `/app/data` + (Cloudron localstorage overwrites it). Re-run after deploy is safe + (idempotent upserts / per-section swaps). **Smoke checklist (acceptance):** @@ -419,6 +429,8 @@ steps below are still required. | Magic link not sent | Mail addon or `SMTP_FROM` | Cloudron mail logs; `CLOUDRON_MAIL_SMTP_*` vars | | Upload `server_misconfigured` | `UPLOAD_ROOT` unset | Set to `/app/data/uploads` (§3) | | Container crash on start | Migration failure | App logs around `prisma migrate deploy` | +| No "Recommended" on method cards | `MethodFacet` not seeded | §10 step 6; API should return `matches.score > 0` for some methods when `facet.*` set | +| `seed.bundle.cjs` ENOENT on `/app/data/...` | Old image without `/app/seed-data` | Deploy ≥ 0.1.8; JSON is at `SEED_DATA_DIR=/app/seed-data` | **Done when:** all smoke checklist items pass. Then proceed to soft-launch (§5 phase 2) and, when ready, [CR-99](https://linear.app/community-rule/issue/CR-99/backend-cloudron-production-install-apex-cutover) diff --git a/prisma/seed/methodFacets.ts b/prisma/seed/methodFacets.ts index 2b7c3ca..5bb6d75 100644 --- a/prisma/seed/methodFacets.ts +++ b/prisma/seed/methodFacets.ts @@ -10,17 +10,14 @@ import { resolveFacetMatch, sectionFacetsSchema, } from "../../lib/server/validation/methodFacetsSchemas"; - -// Bundled seed runs from repo root (`process.cwd()`); __dirname breaks under esbuild. -const REPO_ROOT = process.cwd(); -const DATA_DIR = path.join(REPO_ROOT, "data", "create", "customRule"); +import { methodFacetsJsonDir } from "./seedDataPaths"; /** * Reads + Zod-validates `data/create/customRule/
.json`. * Throws on schema failures so the seed aborts before any DB write. */ async function loadSectionFacets(section: SectionId) { - const file = path.join(DATA_DIR, `${section}.json`); + const file = path.join(methodFacetsJsonDir(), `${section}.json`); const raw = await readFile(file, "utf8"); const parsed = JSON.parse(raw) as unknown; const result = sectionFacetsSchema.safeParse(parsed); @@ -33,7 +30,7 @@ async function loadSectionFacets(section: SectionId) { } async function loadFacetGroups() { - const file = path.join(DATA_DIR, "_facetGroups.json"); + const file = path.join(methodFacetsJsonDir(), "_facetGroups.json"); const raw = await readFile(file, "utf8"); const parsed = JSON.parse(raw) as unknown; const result = facetGroupsFileSchema.safeParse(parsed); diff --git a/prisma/seed/seedDataPaths.ts b/prisma/seed/seedDataPaths.ts new file mode 100644 index 0000000..6b57f9d --- /dev/null +++ b/prisma/seed/seedDataPaths.ts @@ -0,0 +1,22 @@ +import path from "node:path"; + +/** + * Root for committed seed JSON (`data/` in dev; `/app/seed-data` on Cloudron). + * + * Cloudron's localstorage addon mounts `/app/data` at runtime, so facet JSON + * must not live there. The Dockerfile copies `data/` → `/app/seed-data` and + * sets `SEED_DATA_DIR=/app/seed-data`. + */ +export function seedDataRoot(): string { + const override = process.env.SEED_DATA_DIR?.trim(); + if (override) return override; + return path.join(process.cwd(), "data"); +} + +export function methodFacetsJsonDir(): string { + return path.join(seedDataRoot(), "create", "customRule"); +} + +export function templateFacetJsonPath(): string { + return path.join(seedDataRoot(), "templates", "templateFacet.json"); +} diff --git a/prisma/seed/templateFacets.ts b/prisma/seed/templateFacets.ts index 29a69c9..b60a56e 100644 --- a/prisma/seed/templateFacets.ts +++ b/prisma/seed/templateFacets.ts @@ -1,16 +1,8 @@ import { readFile } from "node:fs/promises"; -import path from "node:path"; import type { PrismaClient } from "@prisma/client"; import { FACET_GROUP_IDS } from "../../lib/server/validation/methodFacetsSchemas"; import { templateFacetFileSchema } from "../../lib/server/validation/templateFacetSchema"; - -const REPO_ROOT = process.cwd(); -const TEMPLATE_FACET_FILE = path.join( - REPO_ROOT, - "data", - "templates", - "templateFacet.json", -); +import { templateFacetJsonPath } from "./seedDataPaths"; type TemplateFacetRow = { templateSlug: string; @@ -20,12 +12,13 @@ type TemplateFacetRow = { }; async function loadTemplateFacets() { - const raw = await readFile(TEMPLATE_FACET_FILE, "utf8"); + const templateFacetFile = templateFacetJsonPath(); + const raw = await readFile(templateFacetFile, "utf8"); const parsed = JSON.parse(raw) as unknown; const result = templateFacetFileSchema.safeParse(parsed); if (!result.success) { throw new Error( - `Invalid template facet file ${TEMPLATE_FACET_FILE}: ${JSON.stringify( + `Invalid template facet file ${templateFacetFile}: ${JSON.stringify( result.error.flatten(), null, 2,