7 Commits

Author SHA1 Message Date
aa7387fca8 fix(docker): also copy /app/node_modules/.pnpm so workspace symlinks resolve
Previous fix copied packages/shared/node_modules and
packages/db/node_modules into the runtime stage, but pnpm's layout
makes those entries SYMLINKS into /app/node_modules/.pnpm/<dep>@<ver>/
node_modules/<dep> where the real package files live. Without
.pnpm/ in the runtime image, every symlink dangled and require
still threw 'Cannot find module rrule'.

Add a third COPY for /app/node_modules/.pnpm. Use --link so the
docker layer storage deduplicates against the build layer.

Same root cause class for any pnpm-managed monorepo deploy: ship
.pnpm/ together with the leaf node_modules dirs that point into it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 22:33:46 +08:00
7d3d34af7f fix(docker): copy workspace packages' node_modules into web runtime
Web container booted but every page logged:
  Error: Cannot find module 'rrule'
  at /app/packages/shared/dist/rrule.js

Next's standalone tracer copied packages/shared/dist/*.js into the
standalone output but didn't follow their require("rrule") chain
into packages/shared/node_modules — pnpm's symlinked dep layout
isn't always followable by the tracer. Same risk for any other
shared/db transitive dep (luxon, cron-parser, drizzle-orm, pg,
bcryptjs).

Copy packages/shared/node_modules and packages/db/node_modules into
the runtime stage explicitly so the standalone require chain
resolves. Bot Dockerfile already ships full node_modules so it's
unaffected.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 22:31:14 +08:00
b47c0409ae fix(docker): pass placeholder DATABASE_URL/AUTH_SECRET during web build
Setting the route's `dynamic = "force-dynamic"` only stops Next from
calling the GET handler — not from evaluating the route module. The
bundled route.js inlines lib/db.ts's top-level
createClient(env.DATABASE_URL); next build's "Collecting page data"
pass imports the bundle, env access fires, and Zod throws because
the build container has no DATABASE_URL.

Same story for AUTH_SECRET via actions/auth.ts.

Export both as placeholders inside the RUN layer. pg.Pool is lazy
(stores URL, only connects on first query) and AUTH_SECRET only
matters at sign/verify time, so neither placeholder runs during
build. Each Dockerfile RUN is its own shell — nothing leaks into
the runtime image.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 22:23:49 +08:00
49f5c16b19 fix(docker): reuse node user instead of creating gid 1000 — unblocks publish
Bot + web Dockerfiles tried to addgroup -g 1000 app on top of
node:22-alpine, which already ships a `node` group at gid 1000.
Build aborted at runtime stage 5/5 with:
  addgroup: gid '1000' in use

Drop the addgroup/adduser pair on both images and just chown +
USER node onto the existing node user. Same hardening posture
(non-root, no shell login on the runtime image), one less moving
part. The compose dev overlay's `user: ${HOST_UID:-1000}:${HOST_GID:-1000}`
matches uid 1000 either way.

Plus:
- New docker-compose.portainer.yml: pulls cm-whatsapp-{bot,web}
  from gitea.04080616.xyz/yiekheng instead of building from
  source. Named volumes for sessions / media so the operator
  doesn't need shell access to manage state. Healthchecks on
  both services so Portainer's UI surfaces unhealthy containers.
- New docs/deploy-portainer.md walking through registry auth,
  stack creation, env vars, migrations, first sign-in, future
  redeploys, rollbacks.
- README links the Portainer guide alongside the dev path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 22:09:12 +08:00
b29d137c84 feat: production hardening — robots, allowedOrigins, container non-root, rate limits, CLI bootstrap
robots.ts + metadata.robots blocks indexing.
serverActions.allowedOrigins gates cross-origin Server Action posts.
Bot + web Dockerfiles add a non-root 'app' user (uid 1000) with
chmod 700 on /data/sessions.
sendTestAction grows a per-group rate limit (3/60s).
resumeReminderRunAction + cancelReminderRunAction get a per-IP
rate limit (30/10s).
.env.example documents every required key.
packages/db/src/scripts/{set-password,create-user}.ts + thin shell
wrappers in scripts/ — first admin sets their password via
./scripts/set-password.sh admin before signing in.
2026-05-10 18:05:34 +08:00
8e37beb76b chore: add web Dockerfile and dev compose service 2026-05-09 22:48:48 +08:00
8167872415 chore: add bot Dockerfile and bot service in dev compose 2026-05-09 16:07:46 +08:00