Click "Assign" on any bot row to expand an inline form with a chapter
dropdown. Selecting a chapter and confirming calls POST /api/admin/bots/:id/assign
with the tenantId. The chapter list is loaded alongside bots on mount.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace the home page with a portal selector showing three access paths:
- Organisation Portal → /org/login
- Chapter Portal → /login
- Member Portal → /member-login
Remove /signup and its BFF route — tenants are now only created by org
owners through the org portal or assigned by super admins. Self-serve
community creation is no longer supported.
Update sidebar to bypass auth guard for / (exact match) without
accidentally matching all paths via startsWith.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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 tailscale-redis sidecar sharing Redis network namespace
- Remove public 6380:6379 port mapping — nothing exposed to internet
- Redis now reachable on tailnet as tower-redis:6379 with password auth
- Tailscale uses only outbound connections, so no OCI Security List or
host firewall changes needed
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add requirepass to Redis with REDIS_PASSWORD env var
- Expose port 6379:6379 for external connections
- Update REDIS_URL in api and worker to include password
- Update healthcheck to pass auth flag
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>
826-line doc covering: tech stack, tenancy hierarchy, full message
pipeline (G1+G2), all 55 DB models with field descriptions, 4 auth
realms, all API endpoints, member portal routes, AI dev handoff spec
(queue payloads + how to write ContentDraft output back), environment
variables, repo structure, migration workflow, and known gaps.
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>