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.
The web app is now installable on a phone home screen with offline
fallback for static assets and the navigation shell.
Pieces
------
- `src/app/manifest.webmanifest/route.ts` — dynamic manifest route.
Standalone display mode, portrait orientation, dark theme matching
the app, "any maskable" icons so the same PNG works for both
regular launchers and Android adaptive icons.
- `src/pwa/sw.ts` — service worker entry. Uses serwist's stock
recipe: skipWaiting + clientsClaim so a new worker takes over on
the next navigation, navigationPreload to race the network with
the worker boot, and `defaultCache` for HTML-network-first /
static-cache-first / image+font cache TTLs.
- `next.config.ts` — wraps the existing config with `withSerwistInit`.
Disabled in development (`NODE_ENV !== "production"`) because a
service worker on every dev reload makes hot-reload extremely
flaky.
- `package.json` build script switched to `next build --webpack`.
`@serwist/next` doesn't yet support Turbopack (it logs a warning
and silently skips emitting `sw.js`), and Next 16 defaults the
build to Turbopack. The dev server still uses Turbopack — only
production builds switch to webpack.
- `src/app/layout.tsx` metadata gains `manifest`, `icons.icon` (192
+ 512 PNG), and `icons.apple` (180 PNG). The existing
`appleWebApp.capable` already opts iOS into standalone mode.
Icons
-----
Generated by a tiny one-shot script (`scripts/gen-pwa-icons.ts`)
that uses the workspace's already-installed sharp to render an SVG
wordmark at 512 / 192 / 180 px. Placeholder branding (dark square
with "cm" wordmark) — swap in real artwork later by editing the SVG
in the script and re-running `pnpm --filter @cmbot/web run gen:icons`.
Build artefacts
---------------
- `apps/web/public/icon-512.png`, `icon-192.png`,
`apple-touch-icon.png` ARE committed (stable input).
- `apps/web/public/sw.js` and `swe-worker-*.js` are NOT — they're
regenerated on every production build. Added to `.gitignore`.
Verification
------------
- Production build emits `[serwist] Bundling the service worker
script with the URL '/sw.js' and the scope '/'...` and `sw.js`
shows up in `public/`.
- `/manifest.webmanifest` is in the build's static-route table.
- 249 web tests still passing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Symptom
-------
The upload action rejected anything over 50 MB with a flat
"File too large (>50MB)" — a number that was both too generous for
images (WA caps at 5 MB) and too restrictive for documents (WA
allows 100 MB). And anything over 1 MB was being rejected even
earlier by Next's default Server Action body limit, with a much
less actionable error.
Fix
---
1. New `lib/whatsapp-media.ts` resolves an uploaded file's MIME type
to a WhatsApp delivery kind and validates it against the
per-kind cap that WA actually enforces:
image → 5 MB image/* except sticker-mode
video → 16 MB video/*
audio → 16 MB audio/*
document → 100 MB anything else (PDFs, office docs, …)
Anything not recognised as image/video/audio falls through to
"document", which is also the Baileys sender path the bot uses
to deliver it. So a .zip or .csv ends up correctly classified
AND correctly limited to the document cap.
Error messages now name the kind and show both the actual size
and the cap: "Image too large (5.2 MB > 5.0 MB limit on
WhatsApp)".
2. `next.config.ts` lifts the Server Action body limit from the 1 MB
default to 100 MB, so document uploads actually reach the action
instead of getting bounced at the framework boundary. The WA
per-kind validator inside the action enforces the real limit
from there.
3. The compose-step upload zone hint now reflects the per-kind caps
("Image up to 5 MB · video / audio up to 16 MB · document up to
100 MB") instead of the wrong flat "up to 50 MB" value.
Tests (17 new cases, total 189)
-------------------------------
- classifyMediaKind: image/video/audio prefix routing, fall-through
to document for unknown / empty / octet-stream / text/plain.
- validateForWhatsApp: at-cap, just-under-cap, just-over-cap for
image (5 MB) / video (16 MB) / audio (16 MB) / document (100 MB);
zero-byte rejected; unknown-mime 60 MB upload accepted as document.
- WA_MAX_BYTES sanity: equals the document cap and is >= every other
per-kind limit (so it's safe to use as the framework body cap).
- formatBytes: bytes / KB (no decimals) / MB (one decimal) rendering.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reshape the account lifecycle to match how operators actually want to
work the system:
- Add Account → creates a row with status='unpaired'. No QR yet; the
operator lands on the detail page.
- Pair / Re-pair → transitions an unpaired account to status='pending'
and opens the live QR flow. Works for first-time pair AND for re-pair
of an account that was previously unpaired.
- Unpair → asks the bot to stop the live Baileys session and clean
session files; sets status='unpaired' but KEEPS the row (and its
reminders) so the operator can re-pair without retyping anything.
- Delete → permanently removes the account and cascades to its groups,
reminders, run history.
Schema:
- whatsapp_groups.account_id and reminders.account_id now have
ON DELETE CASCADE so deleting an account fans out cleanly.
UI:
- /accounts list shows everything except the transient 'pending' state.
- /accounts/[id] shows state-aware buttons: Pair (when unpaired/banned/
disconnected), Sync + Unpair (when connected), Delete (always).
- /accounts/new is now an "Add Account" form (label only).
Other fixes:
- next.config.ts: allowedDevOrigins includes 192.168.0.253 +
test/rexwa subdomains so Server Actions work across the LAN.
- packages/shared/src/rrule.ts: rrule@2.8.1 has no exports field and
ships ESM that some bundlers can't resolve via default OR named
import. Use createRequire to bridge — works under both NodeNext
(bot runtime) and Turbopack (web SSR).
Two related fixes:
1. Phone (and any LAN client) couldn't reach the web container because
the dev compose mapped 127.0.0.1:WEB_PORT instead of binding all
interfaces. Drop the loopback prefix.
2. Turbopack and NodeNext disagree on extension handling: bot's tsc
needs `.js` extensions in source imports; Turbopack's transpilePackages
path can't resolve those `.js` requests back to `.ts` source. Switch
to consuming the workspace packages via their compiled dist instead:
- packages/db + packages/shared point `main`/`exports` at ./dist/*
- drop transpilePackages from next.config.ts; web picks up the
compiled `.js` files directly
- dev compose command for web builds shared+db before running
`next dev` so dist is fresh when Turbopack starts
- put the `.js` extensions back in packages/db source so NodeNext
compilers (bot's tsc, packages/db's own tsc) are happy
Three small build-time fixes surfaced when the Docker images first ran
their full production build (previously only dev mode via tsx):
- packages/shared: exclude *.test.ts from tsc (vitest types not needed
for shipped output), add @types/node dep so node:crypto resolves
- packages/db: add @types/node dep for the same reason
- apps/web: pin Next.js Turbopack root to the workspace root via
next.config.ts so the bundler doesn't fail to detect the monorepo
layout from inside the Docker image