236 Commits

Author SHA1 Message Date
2ef64c9192 feat(web): show Re-pair/Delete on accounts list cards
Surface state-aware quick actions directly on each card so the user
doesn't have to drill into the detail page just to delete or re-pair an
account. Re-pair shows when status != connected; Delete (with
destructive confirm dialog) is always available.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 00:35:01 +08:00
4d2531689b chore: stop tracking pre-existing 'session' file 2026-05-10 00:27:49 +08:00
9437df74ee feat(web): split Add Account from Pair; add Unpair/Re-pair/Delete actions
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).
2026-05-10 00:27:33 +08:00
e45bcb581a fix(web,build): consume packages/db + shared via dist; bind web to LAN
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
2026-05-10 00:18:56 +08:00
3d470069d3 feat(web): create reminder + media upload server actions
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 23:48:35 +08:00
6916f5a0ed feat(web): delete reminder server action wired to detail page 2026-05-09 23:46:23 +08:00
83a19d4800 feat(web): send-test server action wired into group detail 2026-05-09 23:44:22 +08:00
68b46f8d71 feat(web): pair / unpair / sync server actions + live QR page 2026-05-09 23:42:16 +08:00
de21edd905 feat(web): settings page with operator info + theme toggle 2026-05-09 23:37:56 +08:00
8fd5468e3a feat(web): reminders list + detail pages with run history 2026-05-09 23:36:18 +08:00
6b1a9191ab feat(web): groups list + group detail pages with trigram search 2026-05-09 23:32:00 +08:00
7708dd671c feat(web): dashboard + accounts list + account detail
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 23:27:24 +08:00
8771e65c8c feat(web): edge middleware deny /api except events + health 2026-05-09 23:15:52 +08:00
1fe674c70e feat(web): SSE endpoint + useEvents hook 2026-05-09 23:11:25 +08:00
63d41c4389 feat(web): app shell with responsive nav + theme provider
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 23:09:33 +08:00
c9960aae24 chore: switch web to port 9000 (test.04080616.xyz) + exclude test files from bot tsc build
- Local dev WEB_PORT now 9000 to match the planned dev/staging domain
  test.04080616.xyz. Production deployment uses port 8100 mapped to
  rexwa.04080616.xyz (configured in plan 4).
- apps/bot/tsconfig.json: exclude src/**/*.test.ts from the production
  build (vitest types only present at dev time, mirrors the same fix we
  made earlier for packages/shared).
2026-05-09 23:07:29 +08:00
17f9ee179f feat(db,web): pg_trgm + indexes + Postgres-backed cache and rate-limit
- Add cacheEntries and rateLimitBuckets tables to schema
- Generate migration 0002_left_jimmy_woo.sql with pg_trgm extension and all indexes
- Implement cache.ts (get/set/delete/getOrSet/sweep) backed by Postgres
- Implement rate-limit.ts (sliding-window UPSERT) backed by Postgres
- Implement search.ts (trigramMatch / trigramRank helpers)
- Add vitest 2.1.9 + vitest.config.ts; 7 unit tests pass (4 cache + 3 rate-limit)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 23:03:10 +08:00
04e3a8d6ed chore: move web from port 3000 to 8100 (avoid conflict with home server containers) 2026-05-09 22:58:25 +08:00
499bcf22ed fix(build): production tsc + Next.js workspace root resolution
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
2026-05-09 22:54:51 +08:00
8e37beb76b chore: add web Dockerfile and dev compose service 2026-05-09 22:48:48 +08:00
2f7313b9ac feat(web): db client, operator helper, IPC notify, logger 2026-05-09 22:48:00 +08:00
7238369503 feat(web): shadcn/ui init + base components
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 22:46:16 +08:00
161ffec84c feat(web): scaffold Next.js 16 app with Tailwind 4 + Geist 2026-05-09 22:40:03 +08:00
21e8e5b582 feat(bot): remove Telegram code; switch to IPC consumer
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 22:37:49 +08:00
af21bc5599 feat(bot): add IPC handlers for pair / unpair / sync / send-test / schedule 2026-05-09 22:34:01 +08:00
abcf19b71a feat(bot): add IPC notify helper + command consumer skeleton 2026-05-09 22:31:40 +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
97099bf28a feat(bot): clean up stale pair flows + 5-min pair timeout
Two related fixes for abandoned pairings:
- After /pair starts a Baileys session, arm a 5-minute timer. If the
  operator doesn't scan in time the bot stops the session, deletes the
  pending account row + session files, and pings them in Telegram.
- On bot startup, sweep any 'pending' account rows older than 1 hour —
  catches the case where the bot was restarted mid-pair, leaving a
  stale row no in-memory state could clean up.
2026-05-09 21:59:48 +08:00
5a775e076b feat(bot): year picker shows current + next 10 years (3 columns) 2026-05-09 18:15:45 +08:00
bafcc5284a feat(bot): trim time menu to Now + Custom only 2026-05-09 18:15:13 +08:00
9e180b65a2 feat(bot): Custom day & time goes straight to year/month/day picker
The preset day list (Today/Tomorrow/+1 week/etc.) was redundant with the
top-level time-quick options (Now / Tomorrow 9 AM / Next Mon 9 AM) and
added an extra step for the operator's actual use case (specific dates).

Tapping "Custom day & time" now opens the year picker directly. Back from
the year picker returns to the time menu (Now / Tomorrow / etc.) instead
of looping into itself.
2026-05-09 18:10:44 +08:00
45fcc11e7b feat(bot): menu-driven year/month/day picker for exact dates
Replace the typed-date input with a fully button-driven calendar:
  Year (current + next 4) → Month (12 buttons, past months disabled)
  → Day (calendar grid for that month, past days disabled)
  → Hour → Minute (existing screens, computed day-offset)

Past months/days render as inert "·" cells with a no-op callback so
operator taps don't error. Year picker covers up to 4 years out — well
beyond the typical reminder horizon.

Replaces the "📝 Specific date…" typed input with "📅 Pick exact date…"
which never asks for keyboard text.
2026-05-09 18:06:11 +08:00
f5666a9d2c feat(bot): more day options + free-text date input
Day picker was limited to ≤1 month. Two enhancements after live testing:
- Add +2 months and +3 months presets
- Add a "📝 Specific date…" option that prompts the operator to type
  YYYY-MM-DD; the bot validates, computes the day-offset, and continues
  straight to the hour picker (rest of the wizard unchanged)

Lets the operator schedule reminders at arbitrary future dates without
expanding the preset list to absurd lengths.
2026-05-09 18:01:11 +08:00
689891dd87 fix(bot): render custom day/hour/minute pickers as plain text
The day picker text included `(timezone: Asia/Kuala_Lumpur)` and the `_`
in the IANA name triggered Markdown's italic delimiter — Telegram's parser
then couldn't find the closing `_` and rejected the message with 400
'can't parse entities at byte offset 62'.

Drop Markdown formatting for all three custom-time picker views (day,
hour, minute) since they include system-generated content (timezones,
day labels, dates) that may contain underscores or other markdown chars.
2026-05-09 17:57:45 +08:00
e9f3fd6e29 fix(db): cascade reminder_runs on reminder delete
Deleting a reminder that had already fired failed with FK violation
'reminder_runs_reminder_id_reminders_id_fk'. Add ON DELETE CASCADE so
the run history is removed alongside its reminder.

reminder_run_targets cascades on run_id (already), so the chain is:
reminder → reminder_runs → reminder_run_targets, all removed in one go.
2026-05-09 17:54:20 +08:00
92deaf1032 fix(scheduler): flip one-off reminders to 'ended' after firing
A fired one-off reminder was staying active forever in the DB and showing
🟢 in the Reminders list. Update reminders.status to 'ended' once a one-off
has fired (regardless of run outcome — one-off is done after one attempt).

Recurring reminders stay 'active' — they have more occurrences pending.
2026-05-09 17:52:36 +08:00
6a221fe043 fix(bot): render Review screen as plain text to avoid Markdown parsing errors
The reminder confirm screen was failing with 'can't parse entities' (400)
because the body string included `[media...]` which Telegram's legacy
Markdown mode tries to interpret as a link `[text](url)` and rejects when
the closing `(url)` isn't present. Same risk for any user-typed body
containing `*`, `_`, backticks, or `[`.

Two fixes:
- Add optional parseMode field to MenuView; showMenu honors it
- reminderConfirmMenu and reminderDetailMenu render as plain text
  (parseMode: undefined) since both include user-supplied content
- Replace `[media...]` brackets with `(media...)` parens in the wizard
  body preview so the placeholder itself can't trigger link parsing
2026-05-09 17:49:00 +08:00
a5bbf3a25d feat(bot): redesign reminder time picker (menu-driven)
Time picker UX changes after live testing:
- Add "🕐 Now" quick option (fires within 30s)
- Remove "🕐 In 1 hour" / "🕒 In 3 hours" — Now + Tomorrow 9 AM cover the
  practical fast-path
- Replace free-text custom date input with a 3-step menu picker:
  Day (Today, Tomorrow, +2d, +3d, +4d, +5d, +1w, +2w, +1m)
  → Hour (24-hour grid, daytime first)
  → Minute (5-min increments)
- Validate the chosen day+hour+minute against "now" and reject if past

Drops parseFreeText path entirely; the wizard's set_time step is gone.
2026-05-09 17:45:08 +08:00
2129403f39 feat(bot): wire reminder wizard + list/detail callbacks
Appends all 9 reminder handler exports to callbacks.ts, creates
commands/reminders.ts, registers the /reminders command, all
callback queries (literal matches before regex catch-alls), wizard
branches in message:text, a media ingest handler, and updates
setMyCommands.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 17:37:11 +08:00
1578f1f948 feat(bot): add reminder menu views (list, detail, wizard steps) 2026-05-09 17:31:55 +08:00
afd5fcb73b feat(bot): add wizard state for reminder creation 2026-05-09 17:30:17 +08:00
01eb5752ee feat(scheduler): add fire-reminder handler + job registration
Also fix rrule default-import workaround so the shared package loads
correctly under NodeNext ESM resolution (rrule@2.8.1 has no exports field).
2026-05-09 17:29:21 +08:00
2ed436ef0e feat(bot): add Telegram media ingest into /data/media 2026-05-09 17:23:59 +08:00
d9a5f5a5e2 feat(bot): extend sender with image/video/document support 2026-05-09 17:23:06 +08:00
1aef3e969c feat(reminders): add time-parsing + CRUD helpers 2026-05-09 17:22:00 +08:00
113adc7edf feat(scheduler): add pg-boss client + lifecycle
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 17:19:01 +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
f613d83b40 docs: add top-level README 2026-05-09 17:10:48 +08:00