Add POST /org/tenants/:id/admins so org admins can provision the first
login for a chapter's admin team. New chapter admins can then sign in at
/login with email + password.
- OrgService.createTenantAdmin — validates chapter belongs to org, hashes
password, creates Admin record
- OrgController POST tenants/:id/admins endpoint
- BFF route /api/org/tenants/[id]/admins
- Add Admin form on /org/tenants/[id] page (email, password, role select)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
/org/* is the org-admin portal — it has its own auth context and should
not redirect to /login. Add ORG_PATHS bypass and render null sidebar
on those routes (same pattern as /my/*).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
/org/login was throwing "useOrgAdmin must be used within <OrgAdminProvider>"
because the provider was never mounted in the app shell.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
- Add OPENROUTER_API_KEY (optional) to env schema so tsc can resolve it
- Add pg + @types/pg to worker deps for outbox-listener
- Explicit Error type on pg client error handler
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Publish every non-spam message to tower:stream:messages immediately
after DB insert in ingest.processor — before any admin approval or
routing decision
- Remove early tags.length === 0 drop in main.ts so no-rule-match
messages are now stored and streamed (monitor-all behaviour)
- Wire classify worker, review/p1/dnc queues into main.ts
- Add streams/message-stream.ts with XADD wrapper (capped at ~50k entries)
- Add docs/DATA_MODELS.md — full data model reference for all 29 models
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>
Adds OnQrCallback and OnStatusCallback parameters to createWhatsAppSession,
broadcasts QR strings and connection status changes to callers, and clears
session files then restarts automatically when the socket is logged out.
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>