13 Commits

Author SHA1 Message Date
082a70db06 docs: consolidate windowed-fanout spec/plan with ETA + paused/resume
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>
2026-05-10 14:33:51 +08:00
d5b8c0beeb feat(reminders): name is required (was optional with auto-derive)
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>
2026-05-10 14:15:16 +08:00
50187a86e1 docs: design spec — windowed, pacing-safe reminder fan-out
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>
2026-05-10 14:01:24 +08:00
d407fc585e docs: manual web app end-to-end test runbook (P3/T23)
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>
2026-05-10 13:22:04 +08:00
24e61f4cdd docs(plan): add Task 9b — Postgres-only cache, rate-limit, search
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.
2026-05-09 22:28:47 +08:00
4b859bc44a docs: add plan 3 — Telegram-free web app
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.
2026-05-09 22:25:43 +08:00
3e2bc8c7ee docs: web app design (Telegram-free pivot, plan 3 spec)
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.
2026-05-09 22:15:51 +08:00
bf3586fe7b docs(plan): add plan 2 — reminder scheduling & sending
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.
2026-05-09 17:15:57 +08:00
33e1fcd2c4 docs: add manual end-to-end pairing test runbook 2026-05-09 16:24:18 +08:00
2122182c56 docs(plan): refactor plan 1 to Docker-first
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>
2026-05-09 15:05:49 +08:00
c3750147eb docs: add plan 1 — foundation & WhatsApp pairing MVP
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>
2026-05-09 14:49:17 +08:00
0f949284b1 docs: lock in subdomain, timezone, retention defaults
Replace open-questions section with confirmed values from brainstorming
review: wabot.04080616.xyz subdomain, Asia/Kuala_Lumpur default timezone,
90-day media retention, 5-minute minimum recurrence interval. Postgres
pg_hba check kept as a pre-deploy verification step.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 14:40:37 +08:00
42caa0d37a docs: initial design spec for WhatsApp reminder bot
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>
2026-05-03 14:36:39 +08:00