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
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.
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>
Group sync previously only ran once at pairing time, so groups created in
WhatsApp afterwards never showed up.
Two complementary fixes:
- 🔄 Refresh button in the groups list view triggers
syncGroupsForAccount() on demand and re-renders the menu
- session.ts now subscribes to Baileys 'groups.upsert' and 'groups.update'
events and re-syncs (debounced 1.5s) so new groups appear without
manual action
Each entry in the groups list is now a button. Tapping shows a group detail
view with [📝 Send Test Text]. Operator replies with the message body and
the bot sends it to the selected WhatsApp group via the live Baileys session,
records the action in audit_log, and shows success/failure inline.
This is a small forerunner of the full reminder send pipeline that plan 2
will build out (with media, scheduling, retries). Useful right now to
validate the end-to-end Telegram-to-WhatsApp send path during pairing tests.
All flows are now reachable from /menu (alias for /start). Single message
edits in place via editMessageText for hierarchical navigation, every leaf
has ⬅ Back / ⬅ Main Menu buttons.
Menu hierarchy:
/menu → main menu
📒 Accounts → list (each account is a button)
📒 <Account> → detail (📂 Groups | 🗑 Unpair | ⬅ back)
📂 Groups → groups list (⬅ back to account, ⬅ main menu)
🗑 Unpair → confirm (✅ yes | ⬅ cancel) → done
📡 Pair New → prompt for label, operator replies as plain message
❓ Help → help text + ⬅ Main Menu
Implementation notes:
- New menus.ts module with pure render functions for each view
- New state.ts tracks pending "awaiting pair label" per Telegram user
- bot.on("message:text") consumes the pending label after Pair New
- /pair, /unpair, /groups commands still work for power users; they reuse
the same handlers behind the scenes (executePairFlow extracted from
handlePair so the menu and the command share one path)
UX improvements driven by live testing:
- setMyCommands populates Telegram's '/' picker with all commands and
descriptions, so the operator gets autocomplete instead of guessing
syntax.
- /start replies with an inline keyboard ([📒 Accounts] [📡 How to Pair]
[❓ Help]) — quick navigation without typing.
- /accounts emits one message per account with [📂 Groups] [🗑 Unpair]
inline buttons. Tapping triggers a callback (no typed labels needed).
- New callbacks module wires the buttons. Unpair shows a confirm/cancel
prompt before acting.
/pair still requires a typed label since the value is operator-defined
content rather than a selection from existing data.