Two changes, together removing nearly all the redundant conversation/history
calls visible in the network panel.
Keep-alive modules: Messenger and Inbox were rendered from the tab ternary, so
every tab change UNMOUNTED them — all their state died and returning re-hit the
API. They now mount on first visit and stay mounted, hidden with CSS. Deliberately
mount-on-first-visit rather than eager-mount-both, so opening the dashboard does
not fire two modules' initial loads for a user who never opens either.
QueryClient ownership: the SDK (0.2.0) takes React Query as a peer and falls back
to its own client if it can't find one — but then its cache would be invisible to
the CRM. Providing it here keeps SDK and CRM data in one store, so a write can
invalidate a conversation list. Client is created in useState, not at module
scope, so an SSR render can't leak one user's cache into another's.
refetchOnWindowFocus is off: the socket already pushes live changes, so focus
refetching was pure noise.
Also declares onnxruntime-web@1.21.0, a PINNED peerDependency of
@imgly/background-removal that npm prunes on any re-resolve. The committed lock
had it, so `npm ci` worked and `npm install` silently broke the gallery build —
declaring it directly makes it unprunable.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Offline-notifications popup was covered by Smart Gallery's toolbar. Raising
its z-index could not have worked: .dash-topbar is `position: sticky; z-index: 20`,
which opens a stacking context, so .tm-menu-pop's z-index: 30 only ranked inside
the topbar. Against module content the whole topbar competed at 20 — and the
vendored photo-gallery SDK stacks up to 1400 (its own README documents this).
Adds AnchoredPopover, which portals to <body> — out of the topbar's stacking
context entirely — positions from the trigger's viewport rect, clamps to the
window edges, and closes on scroll/resize/Escape/outside-click. Applied to the
notification bell AND the user menu, which had the identical trap and would have
been reported next.
Portalling rather than z-index escalation is the fix that survives a module
raising its own z-index later.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Opens from the drawer's "More Details", replacing the placeholder toast.
The board swaps for a full-width lead record; "← Pipeline" goes back.
- Detail page: breadcrumb, hero (avatar, stage/type/storm/age pills,
Call + Email), stage progress bar + 7-step stepper, and an overview
grid of Contact / Property / Job Details / Insurance / Assignment.
- Move Stage dropdown so a lead can be re-staged from the detail page,
not only by dragging on the kanban board. Stage changes now route
through one setStage() used by both paths, so "reached" advancement
and the toast behave identically.
- Detail is keyed by lead id rather than a snapshot, so a stage move
updates the pills, progress, stepper and page accent live.
- pipeline-data: PLead gains phones, propertyType, source, tradeType,
urgency, insurance (null for Retail jobs) and assignment. All derived
deterministically from the existing rows — no Date.now, so SSR and
client render identically.
- Dropdown is a bottom sheet under 720px and an anchored menu above it;
dropped overflow:hidden on .pld-hero, which was clipping it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Picks up the code-chip contrast fix (inline code was invisible on your own
bubbles) and stripMarkup(). Also applies stripMarkup to the notification toast,
which had the same problem — a formatted message toasted as "~crazy~".
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Integrates the Leads and Lead Verification screens with the live be-crm
backend (crm.lead.* / crm.leadVerification.* / crm.media.*) behind a
mode-agnostic data layer that still falls back to the local mock when the
Shell isn't configured.
Data layer (new)
- src/lib/leads-api.ts — crm.lead.search/get/stats/create/update/
updateStatus/assign/sendForVerification + media (presign upload/download)
- src/lib/verify-api.ts — crm.leadVerification.search/get/stats/verify/
markUnverified/assign/reassign/moveToPending
Backend → FE wiring
- Assignee / creator names: read the resolved DTO fields and, as a safety
net, resolve member ids → real names against crm.team.member.search
(be-crm currently echoes the raw principalId as the name).
- Created By: read the createdBy object the backend returns (was blank).
- Site photos: upload via crm.media.presignUpload → PUT, persist through
crm.lead.attachment.add (per photo, after create), render in the detail
popup via crm.media.presignDownload.
- Duplicate guard: surface the backend's real error (detail / duplicate_lead)
instead of the SDK's opaque "data command failed: 400".
UX
- Leads detail: Property Photos gallery + edit-mode photo add/remove.
- Verification: "Change Assignee" now opens a searchable, scrollable popup
instead of a long inline dropdown.
- Removed the static "Storm Zone" tag from lead cards/detail; storm banner
only renders when the lead carries storm data.
Reference / follow-ups
- leads.http, leads.postman_collection.json — be-crm data-door requests.
- LEADS_BACKEND_CHANGES.md — required be-crm changes (name resolution,
assignee carry-over on sendForVerification) verified against :4010.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Resolve conflicts in dashboard.tsx and dashboard.css:
- Keep goutamnextflow's SDK inbox/messenger, settings, notifications,
realtime provider and smart gallery (the old messenger/inbox files
were deleted on that branch)
- Graft the feat/leads additions (Leads, Verify views + their CSS) on top
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Notifications phase 2: a /push-sw.js service worker shows a system notification
on push and, on click, focuses an open CRM tab (or opens one) and deep-links to
the thread. usePushNotifications registers the SW, requests permission, subscribes
with IIOS's VAPID key, and stores the subscription via the be-crm door. The topbar
bell becomes a real enable/disable control (hidden when push is unsupported/demo).
Dashboard listens for the SW's notif-click message + a ?thread= deep link.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Shared RealtimeProvider opens ONE dashboard-wide IIOS message socket reused by
the messenger tab and the new NotificationCenter. CrmMessagingAdapter gains
setFocus (drives presence/push suppression) and subscribeActivity (joins all
of the caller's threads, fans out incoming messages). NotificationCenter shows
a clickable toast on new messages when you're not on the messenger tab and
deep-links to the conversation. Pulls @insignia/iios-messaging-ui@0.1.7.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Topbar search box → debounced crm.search → dropdown of hits (chat/mail, highlighted
snippet). Clicking a hit switches to the right tab and focuses the exact thread:
mail → Inbox, chat → Messenger (via the SDK's new focusThreadId, 0.1.6). Snippet HTML
is escaped except the <em> highlight. Demo mode searches an in-memory set.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the SMTP 'coming soon' stub with a real card: host/port/SSL/user/password/
from-address/from-name → crm.settings.smtp.configure, masked status from
crm.settings.smtp.status. Mirrors the Twilio SMS card; demo mode stores hints locally.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
goutamnextflow adopts @insignia/iios-messaging-ui (drops the bespoke
messenger/inbox/mail) and adds Org Settings → Integrations. Resolve against
the Smart Gallery work:
- dashboard.tsx: route messenger→MessengerSdk, inbox→InboxSdk, add settings→
Settings (goutamnextflow) AND keep gallery→SmartGallery (ours). Drop the
now-deleted Messenger/Inbox imports.
- next.config.ts / package.json: union transpilePackages + deps (keep
@photo-gallery/sdk + ML deps AND @insignia/iios-messaging-ui).
- dashboard.css: keep both appended blocks (.gal* gallery + .settings-* org
settings).
Gallery integrates with zero errors. Vendored @photo-gallery/sdk re-synced
(0-diff). Verified: tsc clean + `next build` compiles the whole merged app
(gallery AI routes + dashboard). The new @insignia/iios-messaging-ui private
dep is stubbed locally (see .claude/dev-scripts); it resolves on CI/Vercel.
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>
- Add Lead source dropdown options (Door Knock, Referral, Storm Chase,
Mailer/Postcard, Sign Call, Insurance Agent Referral, Repeat Customer,
Social Media, Other)
- Quick form: Referral shows a referral-note textarea; Door Knock shows a
canvasser search (by name or email) with picker + chip
- Full form is now a Next/Back wizard ending in Create Lead
- Reps gain email for canvasser search
Co-Authored-By: Claude Opus 4.8 (1M context) <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 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>
Leads: card board with status stats, search + status tabs, a rich
lead-detail popup (contact, property, job, insurance, assignment,
storm banner) and a New Lead intake form (Quick / Full Form) with
multi-phone/email rows, site-photo dropzone and urgency picker.
Lead Verification: clickable stat tiles, status/source/assignee
filters, a full verification table, a detail popup with an activity
timeline, and a per-row actions menu (verify / reassign / pending…).
Both wired into the dashboard view switcher and styled with the
existing orange brand + glass-card system (dark & light themes).
Co-Authored-By: Claude Opus 4.8 (1M context) <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>
- 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).
- 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).
- 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.
- 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).
- 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>
- Team Management: crew hero, member gallery + list, role rings, invites
- Support Center: command-center Overview (live status, pulse ribbon, signal)
- AI Assistant: animated orb rings + textured stage
- Rules: rulebook cards (watermark, uniform orange), 3-up responsive grid
- Brand: --grad-brand now solid rgba(253,169,19,.92), white text/icons on
orange, no orange gradients; dark surfaces (--card-grad/--panel-2) #060608
- Segmented tabs get an on-brand orange active state
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Support Center overview rebuilt as a bento grid: large live-chat hero
tile, support team tile, mini channel tiles and a recent-tickets strip.
- Restore full workspace sidebar (Workspace/Team/Profile groups) with a
premium golden active state; unbuilt modules show a Coming Soon view.
- Use Western names across the UI (James Carter; Emma Wilson, Liam Foster,
Sophie Turner, Noah Mitchell) incl. tickets, threads, portal demo user.
- Remove the Allottee card from Profile; add a clean Account & identity
card and live-editable contacts.
- Relabel "Plot" -> "House number" everywhere; drop the allotment chip in
favour of the property category.
- Premium visual pass: brand gradients, depth, glow accents, pill tabs.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Full dashboard at /dashboard matching the Figma Linkedup-Web frame
- Dark/light theme toggle (persisted to localStorage)
- Live animated charts: stat sparkbars, P&L grouped bars, donut, gauge,
pipeline bars, progress, revenue-potential
- Sidebar, topbar, stat cards, weather, top sales reps
- 29 icons exported from Figma (currentColor) + lucide fallbacks
- Fix: bar charts had zero height (percentage in indefinite flex parent)
- Fix: removed content max-width that left a right-side gap
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>