ValidationPipe forbids unknown properties; the UI sends role so the DTO
must declare it. Pass it through to the service so ORG_OWNER/ORG_ADMIN
is honoured on creation.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add NestJS endpoints the admin panel org UI was calling (all returning 404):
- GET/POST /admin/orgs — list and create organizations
- GET /admin/orgs/:id — org detail with tenants and orgAdmins
- POST /admin/orgs/:id/admins — create OrgAdmin with hashed password
- PATCH /admin/tenants/:id/org — assign/unassign tenant from org
Also ship the member login page and sidebar fix from the previous session:
- Move /my/login to /member-login to break infinite redirect loop
- Add /member-login to PUBLIC_PATHS in sidebar so it is not intercepted
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
Replaces the Redis-over-Tailscale handoff with a far simpler HTTP surface
on the existing public API — no Redis client, Tailscale, or firewall changes
needed on the dev side.
- GET /ai/stream — SSE feed of every ingested message, reads the internal
Redis stream; resumes from Last-Event-ID on reconnect (no missed messages);
heartbeats keep the connection warm through proxies
- POST /ai/drafts — writeback: creates a ContentDraft (PENDING_REVIEW) that
shows up in the admin /admin/drafts review UI
- AiStreamGuard — static bearer token auth (AI_STREAM_TOKEN), accepts header
or ?token= query for browser EventSource
- Remove Tailscale sidecar + public Redis port; Redis stays internal-only
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 GET /accounts (list all accounts) and GET /accounts/:id/qr
(returns QR code as base64 data URL) using the qrcode package.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add jest.clearAllMocks() in beforeEach of both spec files to prevent
call-history state from leaking between tests as the suites grow
- Define GroupSummary interface and use it as explicit return type for
GroupsService.list() to satisfy TypeScript strict typing requirements
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implements TDD-driven GroupsService and GroupsController with full unit test coverage. GET /groups returns all groups ordered by name, selecting id, name, platform, platformId, isActive, and accountId fields.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds Account model (platform, jid, sessionPath, displayName, status) with
AccountStatus enum (ACTIVE/DISCONNECTED/BANNED) and optional accountId FK
on Group for multi-account WhatsApp session tracking.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds packageManager field to root package.json (required by turbo v2.9),
loads .env via dotenv in API jest config so Prisma integration tests find
DATABASE_URL when run from the monorepo root. pnpm build + pnpm test: 8/8 passing.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>