CrmMessagingAdapter.directory() now returns the org people list (Person[]) for the
new 'New message' picker; internal name-resolution uses directoryMap(). The DM/group
create path rides the existing crm.messenger.conversation.open door (dedupe + group).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Delete the now-dead pre-SDK stack (messenger/inbox/mail components + their
*-api libs + messenger-socket) — both tabs render the @insignia/iios-messaging-ui
SDK. Remove the redundant <PageHead> from the messenger/inbox SDK wrappers (the
Topbar already shows the title); the SDK itself renders no header.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
New Settings tab renders an Integrations section; the SMS (Twilio) card lets a
tenant admin connect their own Twilio account (Account SID / write-only Auth Token
/ E.164 from-number) via crm.settings.sms.configure, and shows the masked status
(from-number + SID last-4) from crm.settings.sms.status. Demo mode stores hints
locally. Email (SMTP) shown as the next provider on the same registry.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
uploadAttachment presigns via crm.media.presignUpload + PUTs bytes to IIOS
storage; mailReply/composeInternal/composeExternal forward attachment refs
to crm.mail.reply/internal/send (server already supported them).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the bespoke in-CRM inbox with the SDK's <Inbox>, driven by a new
CrmInboxAdapter over the be-crm data door (crm.inbox.* + crm.mail.*).
Demo mode falls back to the SDK MockInboxAdapter. Maps --miu-* tokens for
.miu-inbox to the CRM theme.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CrmMessagingAdapter now implements the full SDK contract:
- browseChannels/createChannel/leaveChannel over the be-crm channel door
(crm.messenger.channel.*); joinChannel is a governed public self-join over
the socket (openThread) — the BFF has no join verb, OPA enforces it
- listMembers over crm.messenger.members → drives @mention autocomplete
- send forwards mentions[] on the socket path (→ IIOS MENTION inbox items)
So the CRM messenger (rendered by the SDK) now gets sectioned Channels/DMs,
browse+join+create, and @mention autocomplete/highlight — no embedded code.
(SDK re-packed to the local tarball; publish for deploy.) tsc + next build green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CrmMessagingAdapter is now hybrid: BFF for the conversation list, thread
creation, and directory (server-side tenancy); the IIOS MessageSocket
(delegated crm.messenger.realtime token) for everything live — history+join,
send, typing, read receipts, and reaction annotations. Falls back to the 4s
poll only when no socket is available.
- messenger-sdk useRealtimeSocket() opens the MessageSocket from the BFF token
and passes it to the adapter (adapter rebuilds live once connected)
- socket path maps senderId (userId space) → actorId so 'mine'/seen work
consistently, killing the actor-vs-user ambiguity in the poll fallback
- reaction annotations tracked per message and re-emitted as full sets
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The messenger tab now renders the shared SDK's <Messenger>, not the bespoke
in-CRM implementation. The CRM only supplies transport + theme:
- CrmMessagingAdapter implements the SDK's MessagingAdapter over the be-crm
data door (crm.messenger.conversation.list/open, history, send, directory);
live updates via a 4s poll for now (socket realtime is a follow-up that
reuses messenger-socket.tsx). Tenancy/auth stay server-side.
- messenger-sdk.tsx wires MessagingProvider + adapter (live = CrmMessagingAdapter,
demo = the SDK MockAdapter), themed by mapping --miu-* tokens to the CRM
design system.
- dashboard renders <MessengerSdk/>; the old messenger.tsx is left in place
for rollback and will be deleted once this is proven live.
NOTE: the SDK is installed from a local tarball (file:) for verification since
it isn't published yet. For deploy: publish @insignia/iios-messaging-ui to the
registry and change the dep to a version range. transpilePackages covers ESM.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Browsers report an empty file.type for files with no OS-registered MIME (esp.
.md), which then went up as application/octet-stream and got rejected. Infer
the type from the extension for the text types IIOS allows (md/markdown/html/
htm/txt/csv) so those attachments upload.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The appshell SDK useQuery only re-runs when the action changes, not the
variables — so switching mail items (same crm.mail.history action, new
threadId) never refetched and the reader kept the first thread's content
(only the subject, a direct prop, updated). Key MailReader by threadId to
remount it on switch (same pattern Messenger already uses for ThreadView).
Also force a refetch of crm.inbox.list when the filter changes, which hit
the same stale-variables bug.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- inbox merges crm.mail.list threads (kind MAIL) into crm.inbox.list items so
sent/received mail actually appears in the one surface; mail shows in the
Open view (it has no inbox work-item state) and opens in MailReader on click.
Mail rows are read/reply only — no Done/Snooze/Archive (crm.inbox.transition
doesn't apply to a mail thread).
- compose Attachments block was wrapped in <Field> (a <label>), so the label
hijacked the Attach button's click via label→file-input association and the
picker misfired. Use a plain div with the same field styling.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A transformed/overflow panel ancestor was trapping the modal overlay's
position:fixed, so the modal rendered offset inside the messenger/inbox panel
and clipped (Group settings sat inside the thread pane; the compose modal's
top was cut at the panel edge). Portal the overlay up to .dash-root — above
those panels but still inside the scoped design-system CSS — so it centers on
the viewport and the 90vh cap works. Falls back to inline render if no
.dash-root is present.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The modal body is a flex child with overflow-y:auto but had no min-height:0,
so it refused to shrink below content height and pushed the footer past the
90vh cap (visible on the tall Group settings modal — the Done button was
clipped). Add flex:1 1 auto + min-height:0 so the body scrolls and the
footer stays pinned.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- messenger-api: useGroupSettings(threadId) — members query + rename/add/
remove commands + isAdmin (from the member roles); live + mock
- messenger: a settings (gear) button on group thread headers opens a
GroupSettingsModal — editable name (admin), member list with role pills,
admin-gated remove, and add-from-directory search
- controls are admin-gated in the UI; IIOS/OPA re-enforces server-side
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- mail-api: MailMessage carries attachment; reply + sendInternal +
sendExternal accept uploaded attachments; mock updated
- mail.tsx: MailAttachmentView (inline image or file chip via signed URL),
StagedChip; attach button in the reply footer and the New Message
composer (multi-file, up to 10); text optional when a file is attached
- messenger: file-chip icon uses paperclip (was an unknown name)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- media-api: useUploadAttachment (presign → direct PUT to IIOS storage),
useDownloadUrl (short-lived signed URL), isImage, 25MB cap
- socket + REST send now carry attachment {contentRef, mimeType, sizeBytes}
- composer: 📎 attach button, staged chip, send with attachment (text optional)
- MessageBubble renders AttachmentView (inline image or file chip), aligned
to the message side; empty bubble suppressed for attachment-only messages
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Clicking Reply now focuses the composer input (was requiring a manual click).
- A quoted message is clickable → scrolls to the original and flashes it briefly
(was inert). Message elements register refs by id for the jump.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Inbox is the single communication surface — mentions, needs-reply, system
alerts, support updates AND the mail behind them, all in one list. Removed the
separate Mail tab.
- Inbox is now two-pane: the item list (crm.inbox.*) on the left; clicking an item
tied to a thread opens its conversation (MailReader) on the right to read + reply.
Non-threaded items (e.g. system alerts) show their detail. Item actions
(Done/Snooze/Archive) work for every item.
- Compose new mail (in-app or email) from the Inbox header.
- mail.tsx trimmed to reusable MailReader + NewMailModal (no standalone tab);
sidebar + dashboard reverted to no Mail entry.
- The existing work-item Inbox stays the notifier; this makes it the reader too.
HTML bodies still render in a sandboxed iframe. tsc + next build clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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).
gotrue can throw an error whose message is an unhelpful JSON blob (e.g. 500
'Error sending magic link email' when Supabase SMTP isn't configured). Show a
readable, actionable message instead of rendering the raw payload.
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.
The invite link is token-only, so the landing page couldn't tell the invited
email or whether an account existed — it dumped everyone on an empty register
screen. Now it calls a public lookup (/api/invite/lookup → be-crm) and:
- first-time invitee → register with the email prefilled + locked;
- email already registered → sign in with the email prefilled;
- signed-in + registered → accept immediately; signed-in + no profile → onboarding.
Login now redeems the pending invite after sign-in (password/OTP/OAuth), so an
existing user's invite is accepted on login (not only when already logged in).
- 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).
Public, unauthenticated pages required for A2P 10DLC campaign verification:
- /sms-opt-in: web opt-in form (phone field, un-pre-checked consent checkbox,
message description, frequency, msg&data-rates + HELP/STOP disclaimers, links
to Terms & Privacy, 'Yes, sign me up!' submit).
- /privacy: Privacy Policy with the mandatory SMS clauses (no third-party/affiliate
sharing of mobile numbers or SMS consent, message frequency, rates disclosure).
- /terms: Terms of Service with an SMS program-terms section.
Business details centralized in legal-config.ts (replace SMS_SENDER with the
live Twilio number once approved).
- 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.
The Twilio sender isn't delivering, so gate the Verify step behind
NEXT_PUBLIC_MOCK_OTP (default true): 'Send code' skips Supabase/Twilio and a
fixed demo code (123456) verifies, with clear demo-mode copy. The real
addPhone/verifyPhone path is untouched — flip the flag to "false" to restore
live SMS verification.
Replaces the mock verify step (which accepted any 6 digits) with a real SMS
OTP: addPhone() → Supabase/Twilio texts a code → verifyPhone() confirms it.
- Registration creates the auth account when leaving the Account step, so the
phone can be attached + verified against a live Supabase session; finish()
no longer double-registers.
- Onboarding gains a real phone-verify step (Profile → Verify → Address).
- Email is trusted without an OTP in both flows (Supabase auto-confirms on
signup; Google verifies for onboarding), matching project config.
- Bump @abe-kap/appshell-sdk to ^0.2.6.
/portal/onboarding is only a step in the Google sign-in flow. Redirect to
login unless the login page's onboard_email handoff hint is present (and the
session is authenticated), instead of showing an empty form on direct/typed
URL access. Hold rendering until the check passes to avoid a form flash, and
clear the hint once onboarding completes so it can't be reused.
- Onboarding reads the OAuth email (session-storage hint + live Supabase
session via getUserEmail) so Continue is no longer disabled.
- Replace the empty 'Signed in as' banner with a disabled, prefilled
email field (email is fixed — Google-verified).
- Stash the verified email at OAuth callback time for onboarding prefill.
- Bump @abe-kap/appshell-sdk to ^0.2.4 (getUserEmail via getSession).
After a Google sign-in with no CRM profile, route to /portal/onboarding — the
registration flow in mode='onboard': email skipped (Google-verified, prefilled
via getUserEmail), sets a password (updateUser) so email+password login also
works, skips the OTP-verify step, and persists the profile via crm.account.register.
Existing-profile users go straight to the dashboard. Bumps SDK ^0.2.3.
Google is sign-IN only: after OAuth, check crm.account.registrationStatus; if the
account has no CRM profile, sign back out and show an error on the login screen
instead of admitting a profile-less user. (Onboarding to collect the profile comes later.)
- Sidebar footer now shows the ACE identity (was static 'James Carter') with a
Sign-out menu; topbar user gets a Sign-out dropdown too. Both call useAuth().logout.
- Remove all visible 'Demo:' hints from login + register and the 'Demo mode' subtitle.
AuthGate redirected to /portal/login whenever status !== authenticated, which
during boot (before restore() resolves) dropped a valid session on reload. Gate
the redirect on useAppShell().ready so we only bounce once auth has resolved.
The OAuth-return effect ran on mount before AppShell finished booting, so
completeOAuthLogin no-oped (sdk.auth undefined → null) and fell back to identify,
never retrying. Gate it on useAppShell().ready so it runs once auth is up, and
strip ?code= from the URL on success.
- SocialButtons: only Google (Microsoft/Apple hidden).
- Sign-up: remove all social (email/password only).
- Login: keep Google + email/password (single step) + email/SMS OTP; add a direct
'Sign in with a phone number' entry (SMS OTP), and rename the OTP link to
'Sign in with a one-time code'. All Supabase-supported; no mandatory 2FA.
Lift StepAddress/AddressBlock state up so finish() can send the complete
sign-up payload (name, phone, persona, allottee id/names, registered+mailing
address, consent) to be-crm crm.account.register after auth. Config-gated;
non-fatal if the domain persist fails (auth account already created).
- 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).
- dashboard sidebar now uses the --sidebar token so it turns white in
light mode instead of staying black; active nav item stays readable
- registration: drop the Property / Job ID step (job ID + find-by-address),
flow is now Account -> Verify -> Address with steps renumbered
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Replace roles with the 10 Construction Industry personas (Owner, Admin,
Sales Manager, Sales Rep, Project Manager, Site Supervisor, Canvasser,
Team Lead, Subcontractor, Other), each with description, attire &
appearance cues and a common-overlap note
- Render Attire & appearance + Common overlap sections on each role card
- Remap members/invites to new role ids; add canvasser, team-lead and
subcontractor members so every persona shows
- Set color-scheme per theme and style <select> options so the role
dropdown is legible in dark mode
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>