Returning members can now sign in with just their phone number.
The bot DMs them a 6-digit OTP; on verify a 30-day session cookie is set.
First-time users are directed to their invite link from the login page.
- Make OtpChallenge.groupId optional (migration) for re-login challenges
- Add memberLogin / memberVerify service methods
- Add POST /public/auth/member-login and /member-verify controller endpoints
- Add /api/my/login BFF route (sets tower_member_token cookie)
- Add /my/login page (phone → OTP two-step form)
- /my/* now redirects to /my/login instead of /onboard on no session
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds the AI-driven content pipeline so portal content is never manually
created — the system proposes it, admins review and approve.
Worker:
- `tower.event.extract` queue + Event Extractor processor: LLM extracts
event title/date/time/location from messages with #event hashtag,
date/time patterns, RSVP phrases, or location terms; seva action items
within event messages become separate SEVA_TASK drafts
- `tower.faq.candidate` queue + FAQ Curator processor: LLM extracts Q&A
pairs from messages with question patterns + meaningful answer length
- `classify.processor.ts`: `isEventCandidate()` and `isFaqCandidate()`
run after rule matching on all non-spam, non-DNC messages; candidates
are enqueued to the extraction lanes in parallel with normal routing
- `match-rules.ts`: add P1 to TenantRuleRow action union so P1 rules
are fully typed end-to-end
- `NormalizedMessage`: add `quotedPlatformMsgId` field
- `IngestJobData.effectiveAction`: add `'P1'` to the union
Schema:
- `ContentDraft` model with `DraftType` (EVENT|SEVA_TASK|FAQ_ITEM) and
`DraftStatus` (PENDING_REVIEW|APPROVED|REJECTED); carries `proposedJson`
from the LLM, `confidence` score, and `publishedId` after approval
- Migration: `20260617270000_add_content_drafts`
API:
- `DraftsModule`: `GET /admin/drafts`, `POST /admin/drafts/:id/approve`,
`POST /admin/drafts/:id/reject`; approve creates the actual
Event/SevaOpportunity/KnowledgeItem and marks the draft APPROVED;
reject marks it REJECTED — both audit-logged
Web:
- `/admin/drafts` and `/drafts` — tabbed view by type (All/Events/Seva/FAQ)
- `DraftCard` — shows source message excerpt, AI proposed content, and
approve/reject buttons; approve/reject immediately refresh the list
- "AI Drafts" added to both super-admin and tenant-admin sidebars
- "AI Content Drafts" button added to admin dashboard
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Circle + CircleMembership models + migration (CircleRole: MEMBER/LEAD, public/invite-only)
- CirclesModule: admin CRUD + member list; unique circle name per tenant
- Member endpoints in MyController: GET /my/circles, POST /my/circles/:id/join|leave
- listForMember returns public circles + private ones the member belongs to, with isMember/myRole/memberCount
- join() enforces invite-only; leave() removes membership
- /my/circles page: "My circles" + "Discover" split, join/leave button, LEAD + Private badges
- Member + admin BFF routes; Circles nav item
- Register CirclesModule; add CIRCLE_CREATED/DELETED audit actions
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implements the Groups & Routes admin page with a client-side RouteManager
component (add/delete sync routes via fetch), server-side groups page that
pre-fetches groups/routes from the API, and Next.js Route Handler proxies
for /api/routes (GET, POST) and /api/routes/[id] (DELETE). Adds a custom
jest environment that polyfills Node 18+ native fetch into the jsdom sandbox
so tests can use jest.spyOn(global, 'fetch').
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces DisconnectReason enum import with type-only WASocket import and
uses 401 directly instead of DisconnectReason.loggedOut. Baileys is an ES
module that cannot be executed in Jest's CommonJS mode, so removing the
value import (keeping only type imports) prevents ts-jest from trying to
execute the module.
Also updated session-pool.test.ts to verify end() is called with the
expected Boom error object instead of undefined.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>