Per the user's ask: drop the friendly RRULE-based shortcuts (Daily /
Weekly / Custom… etc.) — every selectable preset is now a cron
expression. Schedules are stored in `reminders.rrule` with the
`CRON:` sentinel and dispatched via the existing cron-aware
`nextOccurrence` helper.
Picker
- "Don't repeat" stays at the top (one-off, no cron).
- 11 cron-flavoured presets, each with its underlying cron expression
shown as the hint:
Every minute * * * * *
Every 5 minutes */5 * * * *
Every 15 minutes */15 * * * *
Every 30 minutes */30 * * * *
Every hour at :MM MM * * * *
Every day at HH:MM MM HH * * *
Every weekday at HH:MM MM HH * * 1-5
Every weekend at HH:MM MM HH * * 0,6
Every <DOW> at HH:MM MM HH * * <cron-dow>
Every month on day D at HH:MM MM HH D * *
Every year on Mon D at HH:MM MM HH D M *
- Labels are first-fire-aware: changing the time picker re-derives
every "at HH:MM" label and the preset's canonical cron string.
- "Custom cron expression…" reveals a free-form text input for
anything not covered by the presets.
- Removed: the old "Custom" RRULE detail panel (frequency dropdown,
weekday picker, monthday input, end-condition picker).
Storage
- `presetToSpec("none")` → kind:"none". Every other preset →
kind:"cron" with its canonical cron string.
- `matchPreset` compares the spec's cron expression against each
preset's canonical cron for the current first-fire — falls back to
"cron" (custom textbox) for anything else, including legacy RRULE
reminders that haven't been re-saved yet. Existing RRULE reminders
keep firing on the bot side (nextOccurrence still dispatches both).
- `presetCron(id, firstFire)` is a small pure helper; ISO weekday
(1=Mon..7=Sun) maps to cron weekday (0=Sun..6=Sat).
Tests (+8 in recurrence.test.ts; 137 web + 26 bot + 17 shared = 180)
- presetToSpec emits the right cron for every recurring preset
including Sunday → cron weekday 0.
- matchPreset round-trips through presetToSpec for every preset.
- matchPreset returns "cron" for arbitrary (non-preset) cron strings.
- presetDescriptors lists exactly the cron-only items in order with
first-fire-aware labels ("Every weekday at 09:00", "Every Wed at
09:00", "Every year on May 13 at 09:00", "Custom cron expression…").
- buildRrule produces CRON: prefixed strings for cron presets and
null for "none".
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
cm WhatsApp Reminder Bot
Self-hosted WhatsApp reminder bot. Pairs multiple WhatsApp accounts via Telegram-delivered QR codes and sends scheduled reminders to groups.
Status
Plan 1 complete. Foundation, DB schema, and Telegram-driven WhatsApp pairing are working end-to-end. Reminder scheduling, the web dashboard, and production deploy are upcoming plans (docs/superpowers/plans/).
What's working today:
- Single-operator Telegram bot with a whitelist + audit log of every command.
- BotFather-style menu navigation:
/menuopens a single message that edits in place as you navigate. - Pair a new WhatsApp account with
/menu→ 📡 Pair New → reply with a label. QR is delivered to Telegram and refreshed in place as it expires. - Browse paired accounts with 📒 Accounts. Tap an account → see groups, send a test text message, or unpair.
- Group sync runs at pairing and on every Baileys
groups.upsert/groups.updateevent, plus a manual 🔄 Refresh button. Removed groups are pruned automatically. - Auto-reconnect on transient drops; restart-survival via Baileys
useMultiFileAuthState(no QR rescan needed across container restarts as long as WhatsApp hasn't logged the device out).
Host requirements
Only Docker. No host Node, pnpm, or any other language toolchain — everything runs in containers via the long-lived tools service.
Architecture in one paragraph
Two app containers and one external dependency. bot (Node.js) holds the live Baileys WhatsApp sessions, the grammy Telegram bot, and (in plan 2) a pg-boss scheduler. web (Next.js, plan 3) is stateless UI + API. tools is a long-running Node 22 + pnpm sidecar used for installs/tests/typechecks/migrations so the host doesn't need a Node toolchain. Postgres lives external at 192.168.0.210 in a wabot database. All cross-service communication goes through Postgres (LISTEN/NOTIFY for events, table writes for state).
Full design spec: docs/superpowers/specs/2026-05-03-whatsapp-bot-design.md
Quick start (dev)
Prerequisites: Docker, the wabot database + waBot role on 192.168.0.210 (with a pg_hba.conf line permitting 192.168.0.0/24), and a Telegram bot token from @BotFather.
# 1. Configure env
cp envs/.env.example .env.development
# edit .env.development: real DATABASE_URL, TELEGRAM_BOT_TOKEN, your TG user ID
scripts/gen_auth_secret.sh --write
# 2. Bring up the tools container, install deps
NO_SUDO=1 scripts/dev.sh up
NO_SUDO=1 scripts/dev.sh pnpm install
# 3. Apply migrations and seed your operator row
NO_SUDO=1 scripts/db.sh migrate
NO_SUDO=1 scripts/db.sh seed
# 4. Watch the bot service
NO_SUDO=1 scripts/dev.sh logs bot
In Telegram, message your dev bot /menu, tap 📡 Pair New, reply with a label, scan the QR.
NO_SUDO=1 is the right setting if your user is in the docker group (the default for this repo). Drop it if you need sudo docker.
Layout
apps/bot/— Node service: Baileys WhatsApp + grammy Telegram + (later) pg-boss schedulerapps/web/— Next.js dashboard (plan 3)packages/db/— Drizzle schema and migrationspackages/shared/— cross-app helpers (rrule, media paths, timezones)docs/superpowers/specs/— design specs and manual test runbooksdocs/superpowers/plans/— implementation plansdocker/— Dockerfiles (tools.Dockerfile,bot.Dockerfile,web.Dockerfileplaceholder)scripts/—dev.sh,db.sh,gen_auth_secret.sh, plus stubs for plans 2/4
Scripts
All pnpm/tsx/drizzle-kit invocations run inside the tools container, so no host Node is needed.
| Script | Purpose |
|---|---|
scripts/dev.sh up|down|logs|status|build|exec|pnpm|shell|restart-bot |
Stack lifecycle and tools-container shell |
scripts/db.sh migrate|generate|studio|seed|reset |
Drizzle migration helper |
scripts/gen_auth_secret.sh [--write] |
Generate AUTH_SECRET (host-only, no Node needed) |
scripts/publish.sh |
Push to Gitea registry — implemented in plan 4 |
scripts/link-account.sh |
CLI pairing without Telegram — implemented in plan 2 |
Set NO_SUDO=1 if your user is in the docker group (recommended).
Next plan
docs/superpowers/plans/<next-date>-reminder-scheduling.md — pg-boss, reminder CRUD via Telegram, fire-reminder handler, sender (text/image/video), retry policy, run history.