Drives the work that closes the v1.1.0 production-readiness audit
findings: username + password + role auth on the web app, gated
SSE / QR endpoints, robots/noindex, env hygiene, container non-
root, and rate limits on the four currently-naked Server Actions.
Auth design highlights:
* Roll-our-own session cookie (no NextAuth) — bcrypt password +
HMAC-SHA256 signed cookie; edge-runtime middleware verifies on
every request; defense-in-depth requireUser / requireAdmin in
every Server Action.
* Username + password + 2-role model (admin / user). Schema
migration adds username + password_hash to existing operators
table.
* CLI bootstrap (scripts/set-password.sh) sets the first admin's
password before going live; user management UI gates everything
else.
* OPERATOR_TOKEN_VERSION env var as a global session-invalidation
lever.
* 38 unit tests covering brute-force / cookie tampering / replay /
expiry / fixation / open redirect / timing leak / rate limit /
origin-allowlist / unauth API regression / role gates / self-
demote and last-admin guards.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Folds in three rounds of requirement evolution:
* Pause/resume on window close (was stop-and-report-partial).
* ETA preview pill at compose / edit time so the operator sees
whether their chosen window will fit before scheduling.
* Interactive paused-run banner with Resume / Cancel buttons on the
detail page; pause notification deep-links to it.
Helper relocations:
* windowEndAt() moves to packages/shared so both bot fire-reminder
and the web ETA pill can import the same calculator.
Plan grows from 8 to 10 tasks: adds Task 9 (run-eta + RunEtaPill,
TDD) and Task 10 (resume/cancel actions + PausedRunBanner).
Acceptance gains two paused-flow smoke tests.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously the name field auto-derived from the first text part when
the operator left it blank. That's brittle once reminders carry
multiple parts of varying provenance, and confusing in lists where
"Reminder" or partial sentences crowd in.
Now: every reminder must carry a non-empty name, capped at 60 chars.
- Zod schema on createReminder/updateReminder: name moves from
`z.string().nullable().optional()` to
`z.string().trim().min(1, "Give the reminder a name").max(60)`.
Stale-URL legacy callers that omit it now get a clear server error.
- Wizard compose step: input has `required` + `aria-required`,
placeholder + label simplified ("(optional)" tag and the helper
paragraph removed), Continue blocks on empty.
- Edit-message form: same — required, aria-required, save blocked
on empty, the "leave blank and we'll auto-derive" hint dropped.
- Review-submit client: defensive fail-fast for stale-bookmark URLs
that arrive at step 5 without a name — bounces back with
"Give the reminder a name (back on the Message step)" instead of
letting the server reject.
The resolveReminderName helper stays put — duplicateReminderAction
and any future caller still benefit from the trim+clamp+fallback
chain. Helper unit tests unaffected (they test the resolver in
isolation, the policy-tightening lives at the schema layer above).
298 web tests still passing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Records the design decisions for the next planned work:
- Per-reminder delivery window (default 6am–6pm, operator timezone).
Window-close hard-stops the run; remaining targets become
skipped; status reports as partial with a clear "this account is
at capacity, consider another paired account" message.
- Per-account isolation via pg-boss teamSize ≥ N + an in-process
PerKeyMutex keyed by accountId. Different accounts run in
parallel; the same account serialises (no double-rate sends
that would risk a ban).
- Per-account token-bucket rate limiter (default 40 msg/min,
BOT_MAX_SEND_PER_MINUTE).
- Up-front media-upload cache via prepareWAMessageMedia: 1000
groups × 5 MB upload turns into 5 MB. Biggest single win for
text+picture reminders.
- Bounded group concurrency (default 3 in-flight per account);
parts-within-a-group stay serial for visible message order.
- Pre-fetched DB Maps (groups / messages / media), no inner-loop
round-trips.
- Replaces the rigid 1.5 s inter-part sleep with 200–500 ms
jitter; the per-account rate-limiter is the real gate.
Out of scope for v1 (documented under "v2 candidates"): cross-day
window resume, mid-restart resumability, multi-account auto-split,
adaptive rate-limit back-off, pause/resume mid-run.
Acceptance: 1000-group reminder + one image, established account
finishes in ~30–50 minutes, well inside a 6am–6pm window. Two
reminders on different accounts at the same wall-clock minute
both progress in parallel.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
13 sections covering every flow that unit tests can't:
1. Smoke (mobile header, sidebar, page titles, tab strips)
2. Account pairing (live QR, sync groups, send-test)
3. Re-pair / unpair (status flips, no spurious reconnect)
4. Reminders — single-message one-off (5-step wizard)
5. Reminders — multi-message stack with media swap
6. Reminders — recurrence picker (Daily / Weekly / Monthly /
Yearly + multi-rule, descriptions are sentences not raw cron)
7. Reminders — edit each section preserves the message stack
(regression guard for the parts-2..N drop bug)
8. Reminders list — swipe + lifecycle (no reshuffle on Pause /
Restart, shelf collapses after action)
9. Activity — swipe archive / delete + Archived tab + restore
10. Send-test feedback round trip (Sending… → Sent ✓)
11. Media upload limits (size caps, HEIC/MOV → document fallback)
12. PWA install (standalone display, offline shell)
13. Theme toggle (desktop only)
Captures the policy decisions that aren't immediately obvious from
the code:
- browser-extension hydration warnings are expected, app code is
not at fault — Incognito clears them
- HEIC/MOV uploads are NOT rejected; they fall back to document
delivery
- cron rules render as descriptive sentences in the UI
- status changes don't reorder the reminders list
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Operational rule: cache, queue, search, and rate-limiting all use Postgres
— no Redis or external systems.
New Task 9b adds:
- pg_trgm extension + GIN trigram indexes on whatsapp_groups.name and
reminders.name for fuzzy search
- BRIN indexes on reminder_runs.fired_at and audit_log.created_at for
cheap time-series scans
- Common-filter B-tree indexes on reminders.status and (account_id,
scheduled_at)
- cache_entries table + cacheGet / cacheSet / cacheGetOrSet helpers
- rate_limit_buckets table + checkRateLimit (atomic UPSERT, sliding window)
- search.ts with trigramMatch / trigramRank Drizzle SQL fragments
- Vitest unit tests for cache and rate-limit helpers
Also rewrites Task 12 (rate-limit middleware) to enforce limits inside
Server Actions where DB access exists, rather than edge middleware where
it doesn't.
End-state of plan 3: operator installs the web app as a PWA on their
phone, uses it for everything (pairing with live QR in browser, browsing
groups, sending tests, scheduling reminders). Telegram bot is fully
removed.
Architecture: bot container shrinks (no grammy, no menus); a new
ipc/command-consumer.ts listens to Postgres LISTEN bot.command and
dispatches to existing Baileys/sender/sync logic. New apps/web is
Next.js 16 with Server Components (reads), Server Actions (mutations),
SSE for live updates, and @serwist/next for PWA.
24 tasks across 8 phases (A: Telegram removal, B: web skeleton, C:
foundation, D: read pages, E: mutations, F: reminder wizard, G: PWA,
H: verify + push). UI components delegated to frontend-design skill
during execution.
After live-testing the Telegram bot we hit limits that don't go away with
more menu polish (Markdown fragility, callback_data limits, no native
date pickers, awkward media UX). Pivot to a Next.js PWA installable on
the operator's phone; remove Telegram entirely.
Spec covers: service topology with bot codebase shrunk, no-auth access
stance with rate limiting + reverse-proxy gating, Server Actions
replacing public REST mutation endpoints, SSE for live updates, the new
web-side pair flow with live QR display, multi-step reminder wizard
backed by URL state, mobile-first shadcn/ui visual layer, PWA service
worker via @serwist/next, and a step-by-step plan to delete the existing
Telegram code first.
Inherits all confirmed values from the 2026-05-03 master spec.
End-state of plan 2: operator can schedule one-off reminders via the
Telegram menu wizard, attach text + optional media (photo/video/doc),
and the bot fires them on time to a chosen group. Failed sends retry
with backoff. Run history captured in the DB.
Out of scope (deferred to follow-ups):
- Recurring reminders (RRULE)
- Multi-group / multi-part messages beyond text+1 media
- Run history view in menu
- Web dashboard (plan 3)
9 tasks covering pg-boss client, reminder CRUD helpers, sender refactor
(media), Telegram media ingest, fire-reminder handler, wizard state,
menu views, callback wiring, and end-to-end verification.
Reorganize plan 1 so a long-lived `tools` container running Node 22 + pnpm
is the entry point for every install/test/typecheck/migration command. The
host only needs Docker — no Node or pnpm install required. Tasks reordered
so the tools container exists before any pnpm operation; new tasks added
for the bootstrap install and env-file population.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
End-state of the plan: monorepo, all DB tables migrated, dev Docker stack
running the bot service, and Telegram-driven WhatsApp pairing working
end-to-end (QR delivered, scanned, account connected, groups synced,
auto-reconnect on disconnect, restart-survival via useMultiFileAuthState).
Plans 2-4 (reminder scheduling, web dashboard, production deploy) are
referenced but not yet written.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Captures the validated design from the brainstorming session: two-service
topology (Next.js web + Node bot) communicating via Postgres LISTEN/NOTIFY,
Baileys for WhatsApp, grammy for Telegram, pg-boss for scheduling, Drizzle
for the data model, and Docker/Gitea-registry deploy flow.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>