A Mail surface in the Communication group, distinct from Messenger (chat) and the
work-item Inbox: read app-to-app + email messages (subject + rendered body), reply,
and compose (in-app to a person, or external to an email).
- mail-api.ts: crm.mail.list/history/reply + compose (crm.mail.internal/send), reusing
the messenger directory for the people picker. Live via the AppShell SDK; mock in demo mode.
- mail.tsx: thread list + reader + reply + New-mail composer. HTML bodies render inside a
SANDBOXED iframe (no scripts) — safe against untrusted email HTML.
- Wired into the sidebar (Communication) + dashboard switch.
The existing work-item Inbox stays as the notifier (IIOS's projector already flags new
mail there); this is where you read it. tsc + next build clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Wire the IIOS kernel client's existing realtime affordances into the
Messenger UI and fix three display issues.
- Bubble contrast: incoming bubbles used --panel-2, which equals --bg in
the dark theme (both #060608) → invisible. Use --panel + a border.
- DM title: mapped all participants (incl. self → unknown-id fallback),
producing "User d10888, Maaz Ahmed". Now shows the counterpart only.
- Live sidebar preview: lastMessage/time update on any inbound socket
message, reconciled with a debounced conversation-list refetch.
- Typing indicator: throttled typing() send + auto-expiring "typing…" line.
- Read receipts: markRead() on open/new message; "Sent"/"Seen" under the
last outgoing message. Receipt event carries no threadId, so it is a
global stream filtered to my own messages by actor id.
- Reactions: emoji picker on hover, chips with counts, live via annotation.
- Reply/quote: parentInteractionId round-trips; quoted parent renders above
the reply and in a composer quote bar.
Presence (online + last-seen) is intentionally not included — the kernel
has no presence receive-event yet; that needs an IIOS change + client release.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
New "Communication" area in the dashboard, built on the existing appshell-sdk wiring
(useQuery/sdk.command) like Team, with the IIOS MessageSocket for live streaming.
- messenger-api.ts / inbox-api.ts: mock (demo) + live (crm.messenger.* / crm.inbox.*)
behind one interface, switched by isShellConfigured().
- messenger-socket.tsx: MessengerSocketProvider — one IIOS MessageSocket per panel
(openThread history + live on("message") + send). REST 4s poll is the automatic
fallback when the socket isn't connected.
- messenger.tsx: conversation list ⇄ thread + composer + new-chat people picker
(DM 1 person / group 2+); inbox.tsx: filterable feed with Done/Snooze/Archive.
- sidebar: Communication group (Messenger + Inbox, always-visible); dashboard: panel switch.
- .npmrc: add the @insignia Gitea registry for @insignia/iios-kernel-client.
Works on mock immediately; goes live once NEXT_PUBLIC_SUPABASE_URL + the BFF + be-crm are set.
tsc clean; next build passes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Per AppShell, email delivery lives in the domain API. Delete the frontend
/api/email/invite route + invite-email template, and the team-management email
wiring. be-crm now emails invitees on create/resend. Copy link stays as a
fallback (invite token still returned in the pending-invite list).
Use displayName + email now returned by be-crm member search (from CRM
registration). Falls back to the ACE for the current user, then job title —
never the raw principal id as the email.
- POST /api/email/invite: sends the invite email via the Twilio Emails API using
server-only TWILIO_ACCOUNT_SID/TWILIO_AUTH_TOKEN (Vercel env), from
EMAIL_FROM_ADDRESS (default support@lynkedup.dev). Fixed invite template; gated
on a session cookie so it isn't an open relay.
- Team Management emails the invite on create and resend (token now returned by
both), with the Copy-link as a fallback. Mock mode skips email.
- Registration no longer asks for a role/persona (removed the role select +
allottee block); crm.account.register sends no persona.
- New users have no permissions → the sidebar now shows only Dashboard + Profile
for them, gated on crm.account.me (membership + permissions). Members see the
areas their permissions allow.
- Add /portal/invite?token=… : accepts the invite for a signed-in registered
user, or routes an unregistered invitee through register/onboarding, which
redeems the stashed token on completion (granting the invited role).
- Team Management: 'Copy link' on pending invites builds the invite link from
the invitation token (no email delivery yet).
- Login: identify → password/otp now push() history (was replace()), so Back
returns to the email screen instead of leaving to /register.
- Login: in Shell mode the OTP screen no longer shows an email/SMS toggle —
the channel is fixed by how you signed in, so email sign-in never shows a
stray SMS option.
- Login: hide phone (SMS) sign-in while OTP is mocked — a faked login code
can't mint a session (AuthGate would bounce), so it can't work without SMS.
Email OTP + password + Google stay real and working.
- Register: add Back buttons (email screen → sign in; profile screen → email).
- Share MOCK_OTP/DEMO_OTP via @/lib/otp.
- team-management: new useTeamData() data layer serves live be-crm data door
(crm.team.member.search/role.list/invitation.list + set-roles/perm/invite
commands) or the mock fallback, gated on isShellConfigured(). A member can
hold MANY roles: role chips + multi-select picker in Edit/Invite modals.
- login OTP: OtpVerify wired to real Supabase email + SMS OTP (passwordless)
via appshell sendEmailOtp/verifyEmailOtp + sendPhoneOtp/verifyPhoneOtp.
- bump @abe-kap/appshell-sdk ^0.2.0 (phone OTP).