THE UNIFIED PLATFORM BIBLE — ADDENDUM 06
Migration × Contacts × WebaBox — Multi-Contact Manager
Version: 1.0
Date: June 6, 2026
Author: Alain Bessette, CFO/Product Owner, ABI Conglomerate
Domain: GoWeBa.com
Status: Official Strategic Addendum — Binding Specification
Constraints: ZERO data loss · ZERO force-reset · ZERO email send/receive disruption
⚠️ CRITICAL NOTICE
This addendum has the SAME authority as Part 1, Part 2, and all preceding Addendums of the Bible.
All changes described herein are production-deployed and checkpointed.
No schema-breaking changes were made. All modifications are backward-compatible.
📋 TABLE OF CONTENTS
- Phase A — WebaBox Category Fix
- Phase B.1 — Migration Delete Button
- Phase B.2 — Google Account Disconnect
- Phase B.3 — Auto-Enrichment During Migration
- Phase B.4 — WebaCategory Classification Engine + 90-Day Archiving
- Phase C.1 — Contact Timeline Tab
- Phase C.2 — Attachments Tab (Color-Coded + Preview + Fullscreen)
- Phase C.3 — Merge Log Tab
- Phase C.4 — AI Insights Tab
- Phase D — Post-Migration Batch Enrichment & Verification
- Phase E — Multi-Contact Manager (Relational Enrichment + Merge Safety)
- Bug Fixes & Corrections
- Files Modified — Complete Inventory
- Checkpoints Timeline
<a id="phase-a"></a>
SECTION 1: PHASE A — WEBABOX CATEGORY FIX
1.1 Problem Statement
The WebaBox PATCH API (/api/inbox/threads/[id]) was setting webaCategory: 'CONVERSATIONS' when a user replied to an URGENT thread. However, CONVERSATIONS is not a valid value in the WebaCategory enum.
Valid enum values: URGENT, IMPORTANT, JOURNAL, OFFICE, NONE.
1.2 Root Cause
In webabox-client.tsx, the reply handler was hardcoding webaCategory: 'CONVERSATIONS' instead of the correct webaCategory: 'NONE'.
1.3 Fix Applied
File: app/(app)/webabox/webabox-client.tsx
Change: webaCategory: 'CONVERSATIONS' → webaCategory: 'NONE'
1.4 Behavior After Fix
| Action | Before | After |
|---|---|---|
| Reply to URGENT thread | Thread moves to "Conversations" with invalid category | Thread moves to Conversations filter (NONE + messageCount > 1) |
| WebaBox card counts | Could show phantom counts | Accurate counts across all 12 cards |
1.5 Key Clarification — WebaBox 12-Card Architecture
The 12 WebaBox cards use 5 DB values + 7 smart filters:
| Card | Source |
|---|---|
| Urgent | webaCategory = URGENT |
| Important | webaCategory = IMPORTANT |
| Journal | webaCategory = JOURNAL |
| Office | webaCategory = OFFICE |
| None | webaCategory = NONE AND messageCount = 1 |
| Conversations | webaCategory = NONE AND messageCount > 1 |
| Reply Later | isReplyLater = true |
| Set Aside | isSetAside = true |
| Starred | isStarred = true |
| Spam | isSpam = true |
| Trash | deletedAt IS NOT NULL |
| All Emails | All non-deleted threads |
<a id="phase-b1"></a>
SECTION 2: PHASE B.1 — MIGRATION DELETE BUTTON
2.1 Problem Statement
The Delete button on migration project cards was only visible for CREATED, COMPLETED, FAILED, and CANCELLED statuses. Projects with IMPORTING, SCANNING, or PLANNING status showed no Delete button — leaving users unable to cancel a stuck migration.
2.2 Changes Applied
Frontend — app/(app)/migration/migration-client.tsx
// Before
const canDelete = ['CREATED', 'COMPLETED', 'FAILED', 'CANCELLED'].includes(project.status);
// After
const canDelete = ['CREATED', 'COMPLETED', 'FAILED', 'CANCELLED', 'IMPORTING', 'SCANNING', 'PLANNING'].includes(project.status);
Backend — app/api/migration/projects/[id]/route.ts
// Before
const deletableStatuses = ['CREATED', 'COMPLETED', 'FAILED', 'CANCELLED'];
// After
const deletableStatuses = ['CREATED', 'COMPLETED', 'FAILED', 'CANCELLED', 'IMPORTING', 'SCANNING', 'PLANNING'];
2.3 Result
Users can now delete migration projects in any status, including active imports.
<a id="phase-b2"></a>
SECTION 3: PHASE B.2 — GOOGLE ACCOUNT DISCONNECT
Already existed in the platform. The Disconnect button was functional on the Migration page. No changes needed.
<a id="phase-b3"></a>
SECTION 4: PHASE B.3 — AUTO-ENRICHMENT DURING MIGRATION
4.1 Overview
When contacts are imported during a Google Workspace migration, the system now automatically enriches them by:
- Creating/linking
CompanyContactrecords based on company name or email domain - Creating
ContactCompanyRolelinks between Contact and CompanyContact - Analyzing email signatures via LLM to extract additional data
4.2 New Functions
File: lib/migration/migration-actions.ts
enrichContactPostCreation(contactId, normalizedContact, organizationId, userId)
- Trigger: Called fire-and-forget after
createContactEntity()in the migration execution flow - Logic:
- If
companyNameis present → find or createCompanyContact(matching by name case-insensitive OR email domain) - Extract email domain, skip generic providers (gmail, yahoo, hotmail, etc.)
- Create
ContactCompanyRolelink with jobTitle as role
- If
- Error handling: Fire-and-forget — failures logged but never break migration
analyzeSignatureWithLLM(bodyHtml, bodyText)
- Purpose: Extract structured data from email signatures
- Model:
primary(gpt-4.1) via WEBA LLM wrapper - Extracted fields:
jobTitlecompanyNamephonewebsiteaddressbookingUrl← NEW (Calendly, Cal.com, HubSpot meetings, Acuity, etc.)
- Strategy: Takes last 600 chars of email body as signature block
- Output: JSON parsed response, returns
nullif no useful data found
4.3 Integration Point
File: app/api/migration/projects/[id]/execute/route.ts
// After createContactEntity()
enrichContactPostCreation(contact.id, normalized, organizationId, userId).catch(() => {});
<a id="phase-b4"></a>
SECTION 5: PHASE B.4 — WEBACATEGORY CLASSIFICATION ENGINE + 90-DAY ARCHIVING
5.1 Classification Engine
File: lib/migration/weba-classify.ts
Rules-based classification with optional LLM fallback for ambiguous threads.
Rules Engine (No LLM cost)
| Category | Regex/Rule Patterns |
|---|---|
| JOURNAL | DMARC reports, noreply@, no-reply@, receipts, shipping notifications, delivery confirmations, system alerts, autoresponders, bounce messages |
| OFFICE | Newsletters, social media notifications, promotional emails, "unsubscribe", Gmail category labels (CATEGORY_PROMOTIONS, CATEGORY_SOCIAL, CATEGORY_UPDATES, CATEGORY_FORUMS) |
| URGENT | Gmail starred messages, IMPORTANT label |
| IMPORTANT | Contacts known in org (email in ContactEmail table), threads with 3+ messages (active conversations) |
| NONE | Default fallback |
LLM Fallback (Optional)
classifyAmbiguousWithLLM() — Batch-classifies threads that got NONE with low confidence. Available for post-migration pass but not called during initial import to keep costs down.
5.2 Integration Point
File: app/api/migration/projects/[id]/execute/route.ts
// Before thread creation
const webaCategory = classifyWebaCategory(threadData, gmailLabels, orgContactEmails);
// webaCategory passed to EmailThread.create()
5.3 90-Day Archiving API
File: app/api/migration/archive-old-threads/route.ts
- Endpoint:
POST /api/migration/archive-old-threads - Body:
{ days?: number }(default: 90) - Action: Sets
isArchived: trueon threads older than N days - Audit: Logged as
migration.archive_old_threads
<a id="phase-c1"></a>
SECTION 6: PHASE C.1 — CONTACT TIMELINE TAB
6.1 New Tab
Files:
app/api/contacts/[id]/timeline/route.ts— API endpointapp/(app)/contacts/[id]/timeline-tab.tsx— UI component
6.2 Features
- Chronological list of all email exchanges with the contact
- Aggregates across ALL contact email addresses (primary + ContactEmail records)
- Color-coded by direction:
- INBOUND ↓ — Green indicators
- OUTBOUND ↑ — Blue indicators
- Subject, snippet, date, from/to addresses displayed
- PDF export capability
<a id="phase-c2"></a>
SECTION 7: PHASE C.2 — ATTACHMENTS TAB (COLOR-CODED + PREVIEW + FULLSCREEN)
7.1 Complete Rewrite
File: app/(app)/contacts/[id]/attachments-tab.tsx
7.2 Features
- Lists all email attachments from the contact's threads
- Direction color-coding: Green badges for INBOUND, Blue for OUTBOUND
- Preview button (👁): Opens a dialog with inline preview for:
- Images (all formats)
- PDFs (via iframe)
- Video (HTML5 player)
- Audio (HTML5 player)
- Text files (monospace display)
- Fullscreen mode (⛶): Maximize button in preview dialog → immersive black overlay
- Download button: Available from both the list and the preview modal
- Preview API: Uses existing
/api/inbox/attachments/[id]?action=previewfor presigned inline URLs
<a id="phase-c3"></a>
SECTION 8: PHASE C.3 — MERGE LOG TAB
8.1 New Tab
Files:
app/api/contacts/[id]/merge-log/route.ts— API endpointapp/(app)/contacts/[id]/merge-log-tab.tsx— UI component
8.2 Features
- Shows history of all merge operations involving this contact
- Displays: merge date, source contacts, actor (who performed the merge)
- Data sourced from AuditLog records with action
contact.merge
<a id="phase-c4"></a>
SECTION 9: PHASE C.4 — AI INSIGHTS TAB
9.1 Architecture
API: app/api/contacts/[id]/ai-insights/route.ts
UI: app/(app)/contacts/[id]/ai-insights-tab.tsx
9.2 LLM Analysis
- Trigger: On-demand — user clicks "Lancer l'analyse" button
- Data gathered: Contact details, all email addresses, phones, company roles, recent interactions (subject + body), notes
- Model:
primary(gpt-4.1) via WEBA LLM wrapper - Prompt: Structured French prompt requesting:
- Résumé de la relation
- Points clés de communication
- Statistiques d'interaction
- Recommandations
- Signaux de risque ou d'opportunité
9.3 UI Features
- "Lancer l'analyse" CTA with gradient styling
- Loading state with animated spinner
- Parsed markdown sections (headers, bullets)
- Stats bar: email count, response time, tokens used
- "Réanalyser" button for refresh
- Tab color: Violet accent (consistent with AI features)
<a id="phase-d"></a>
SECTION 10: PHASE D — POST-MIGRATION BATCH ENRICHMENT & VERIFICATION
10.1 Batch Enrichment API
File: app/api/migration/batch-enrich/route.ts
- Endpoint:
POST /api/migration/batch-enrich - Purpose: Enrich contacts missing
jobTitleANDcompanyNameby analyzing their email signatures - Flow:
- Find contacts without jobTitle/companyName (limit: 50-200 per batch)
- For each contact, find the latest INBOUND email (via thread link or participantEmails)
- Call
analyzeSignatureWithLLM()on the email body - Update contact fields (jobTitle, companyName, website, calendarBookingUrl)
- Create ContactPhone record if phone extracted
- Create CompanyContact + ContactCompanyRole if company found
- Deduplication: Checks existing records before creating
- Audit: Logged as
migration.batch_enrich
10.2 Post-Verification API
File: app/api/migration/post-verify/route.ts
- Endpoint:
POST /api/migration/post-verify - Purpose: Run comprehensive post-migration health check
- Returns:
- Duplicate analysis: Uses existing
detectDuplicates()(email, name, phone matching) - Completeness stats: Counts of contacts missing jobTitle, companyName, phone, email
- Company links: Count of contacts with CompanyContact associations
- Thread stats: Total, archived, WebaCategory distribution
- Project stats: If
projectIdprovided, returns migration project status
- Duplicate analysis: Uses existing
- Audit: Logged as
migration.post_verify
<a id="phase-e"></a>
SECTION 11: PHASE E — MULTI-CONTACT MANAGER
11.1 Vision
A single contact can have:
- 4+ companies (via ContactCompanyRole → CompanyContact)
- 4+ phone numbers (via ContactPhone records)
- 5-6 emails (via ContactEmail records)
- Multiple addresses (via ContactAddress records — offices, properties, international)
- Multiple social profiles (via ContactSocialMedia records)
- Booking URL (calendarBookingUrl field)
11.2 Problem Identified
The Signature Intelligence was extracting data correctly but saving it to flat fields only:
| Data | Saved to (❌ Before) | Tab reads from |
|---|---|---|
| Phone | Contact.primaryPhone (text) | ContactPhone (relational table) |
| Company | Contact.companyName (text) | ContactCompanyRole → CompanyContact (relations) |
| Facebook/Instagram | Contact.socialLinks (JSON) | ContactSocialMedia (relational table) |
Result: Tabs showed (0) despite data being extracted.
11.3 Fix — E.1: Relational Enrichment
File: app/api/email/extract-signature/route.ts
The Signature Intel API now creates relational records in addition to flat fields:
ContactPhone Creation
- Creates
ContactPhonefor extractedphone(label: work) andmobilePhone(label: mobile) - Sets
isPrimary: truefor first phone if contact has no existing phones - Deduplication: checks existing ContactPhone records before creating
CompanyContact + ContactCompanyRole
- Finds existing CompanyContact by name (case-insensitive) or email domain
- Creates new CompanyContact if not found (with domain, website)
- Creates ContactCompanyRole link with jobTitle as role/title
- Sets
isPrimary: truefor first company role
ContactSocialMedia
- Creates individual records for each social platform found:
- LinkedIn →
LINKEDIN - Twitter/X →
TWITTER_X - Facebook →
FACEBOOK - Instagram →
INSTAGRAM - GitHub →
GITHUB - YouTube →
YOUTUBE - TikTok →
TIKTOK
- LinkedIn →
- Handle extracted from URL (strips protocol/www)
- Deduplication: checks
platform:handlecombination
11.4 Fix — E.2: Merge Deduplication (ContactCompanyRole)
File: lib/contacts/contact-merge.ts
Before: updateMany moved ALL ContactCompanyRole records → CRASH on unique constraint contactId_companyContactId if target and source share the same company.
After: Per-record check:
// For each source role:
if (targetRoleSet.has(role.companyContactId)) {
// Same company → delete the duplicate from source
await prisma.contactCompanyRole.delete({ where: { id: role.id } });
} else {
// Different company → move to target
await prisma.contactCompanyRole.update({ where: { id: role.id }, data: { contactId: targetId } });
}
11.5 Fix — E.3: Merge Deduplication (ContactSocialMedia)
File: lib/contacts/contact-merge.ts
Same pattern — checks platform:handle combination before moving:
if (targetSocialSet.has(`${s.platform}:${s.handle.toLowerCase()}`)) {
await prisma.contactSocialMedia.delete({ where: { id: s.id } });
} else {
await prisma.contactSocialMedia.update({ where: { id: s.id }, data: { contactId: targetId } });
}
11.6 Fix — E.4: Target Inherits Best Fields After Merge
File: lib/contacts/contact-merge.ts
After merge, the target contact now inherits the best available data from merged sources:
for (const source of sources) {
if (!target.jobTitle && source.jobTitle) updates.jobTitle = source.jobTitle;
if (!target.companyName && source.companyName) updates.companyName = source.companyName;
if (!target.website && source.website) updates.website = source.website;
if (!target.calendarBookingUrl && source.calendarBookingUrl) updates.calendarBookingUrl = source.calendarBookingUrl;
if (!target.socialLinks && source.socialLinks) updates.socialLinks = source.socialLinks;
}
11.7 Complete Merge Flow (16 Steps)
When merging N contacts into 1 target:
| # | Relation | Strategy |
|---|---|---|
| 1 | ContactEmail | Move non-duplicate, delete dupes |
| 2 | ContactPhone | Move non-duplicate, delete dupes |
| 3 | ContactInteraction | Move all |
| 4 | ContactNote | Move all |
| 5 | ContactTag | Move non-duplicate, delete dupes |
| 6 | ContactAddress | Move all |
| 7 | ContactSocialMedia | Move non-duplicate, delete dupes (E.3) |
| 8 | ContactCompanyRole | Move non-duplicate, delete dupes (E.2) |
| 9 | ContactGroupMember | Move non-duplicate, delete dupes |
| 10 | ContactObjectLink | Move (skip dupes via unique constraint) |
| 11 | FileAsset | Re-link all |
| 12 | ClientInvoice | Re-link all |
| 13 | EmailThread | Re-link all |
| 14 | StudioFormSubmission | Re-link all |
| 15 | ContractSignatory | Re-link all |
| 16 | CalendarEventAttendee | Re-link all |
Then: set mergedIntoId on sources, inherit best fields on target.
<a id="bugfixes"></a>
SECTION 12: BUG FIXES & CORRECTIONS
12.1 Timeline Tab — Infinite Loading Loop
Root cause: useT() translation function t was included in useCallback dependencies. The t function changes reference on each render, causing infinite re-fetch loops.
Fix: Removed t from useCallback deps array in timeline-tab.tsx.
12.2 Merge Log Tab — Infinite Loading Loop
Same root cause and fix as Timeline Tab, in merge-log-tab.tsx.
12.3 Delete API — IMPORTING Status Blocked
Problem: Backend API refused deletion for IMPORTING/SCANNING/PLANNING projects.
Fix: Added these statuses to deletableStatuses array in app/api/migration/projects/[id]/route.ts.
12.4 BookingUrl Not Extracted
Problem: analyzeSignatureWithLLM() did not extract calendar booking URLs.
Fix: Added bookingUrl to the LLM prompt, type signature, hasData check, and applyEnrichment(). Maps to Contact.calendarBookingUrl.
<a id="files"></a>
SECTION 13: FILES MODIFIED — COMPLETE INVENTORY
New Files Created
| File | Purpose |
|---|---|
lib/migration/weba-classify.ts | WebaCategory classification engine (rules + LLM) |
app/api/migration/archive-old-threads/route.ts | 90-day thread archiving API |
app/api/migration/batch-enrich/route.ts | Post-migration batch enrichment API |
app/api/migration/post-verify/route.ts | Post-migration verification API |
app/api/contacts/[id]/ai-insights/route.ts | AI Insights analysis API |
app/(app)/contacts/[id]/ai-insights-tab.tsx | AI Insights UI component |
Modified Files
| File | Changes |
|---|---|
app/(app)/webabox/webabox-client.tsx | CONVERSATIONS → NONE fix |
app/(app)/migration/migration-client.tsx | Added IMPORTING/SCANNING/PLANNING to canDelete |
app/api/migration/projects/[id]/route.ts | Added IMPORTING/SCANNING/PLANNING to deletableStatuses |
app/api/migration/projects/[id]/execute/route.ts | Added enrichContactPostCreation + classifyWebaCategory calls |
lib/migration/migration-actions.ts | Added enrichContactPostCreation() + analyzeSignatureWithLLM() + bookingUrl |
app/(app)/contacts/[id]/timeline-tab.tsx | Fixed infinite loop (removed t from useCallback deps) |
app/(app)/contacts/[id]/merge-log-tab.tsx | Fixed infinite loop (removed t from useCallback deps) |
app/(app)/contacts/[id]/attachments-tab.tsx | Complete rewrite: preview + fullscreen modal |
app/(app)/contacts/[id]/contact-detail-client.tsx | Added AI Insights tab |
app/api/email/extract-signature/route.ts | Added relational enrichment (ContactPhone, CompanyContact, ContactSocialMedia) |
lib/weba/signature-extraction.ts | Reference — buildContactEnrichmentData (unchanged) |
lib/contacts/contact-merge.ts | E.2/E.3: dedup ContactCompanyRole + ContactSocialMedia; E.4: inherit best fields |
<a id="checkpoints"></a>
SECTION 14: CHECKPOINTS TIMELINE
| # | Checkpoint | Description |
|---|---|---|
| 1 | Phase A fix | URGENT → NONE (not CONVERSATIONS) in webabox-client.tsx |
| 2 | Phase B.1-B.2 | Delete button visible + Google Disconnect (pre-existing) |
| 3 | Phase C.1-C.3 | Timeline, Attachments (color-coded), Merge Log tabs |
| 4 | Fix delete timeline mergelog preview | IMPORTING delete + infinite loop fixes + Attachments preview/fullscreen |
| 5 | C.4 AI Insights tab contacts | On-demand LLM analysis tab |
| 6 | B.3 auto-enrich CompanyContact migration | Auto-enrichment during migration execution |
| 7 | B.4 WebaCategory classify archive 90j | Classification engine + archiving API |
| 8 | D batch-enrich post-verify APIs | Batch enrichment + verification endpoints |
| 9 | Fix delete API allow IMPORTING status | Backend API fix for IMPORTING deletion |
| 10 | Add bookingUrl extraction from signatures | BookingUrl added to LLM signature extraction |
| 11 | E multi-contact relational enrich merge | Relational enrichment + merge deduplication |
SECTION 15: SCHEMA IMPACT
Zero Schema Changes
All modifications use existing Prisma models and fields:
Contact— jobTitle, companyName, website, calendarBookingUrl, socialLinks, primaryPhone, primaryEmailContactPhone— phone, label, isPrimaryContactEmail— email, isPrimaryContactCompanyRole— contactId, companyContactId, role, title, isPrimaryCompanyContact— name, domain, website, organizationIdContactSocialMedia— platform (SocialPlatform enum), handle, url, isPrimaryContactAddress— full address fieldsEmailThread— webaCategory (WebaCategory enum), isArchivedContactInteraction— subject, bodyContactNote— content
No prisma db push required. No --force-reset. No --accept-data-loss.
SECTION 16: API ENDPOINTS CREATED
| Method | Endpoint | Purpose |
|---|---|---|
| POST | /api/migration/batch-enrich | Batch-enrich contacts missing jobTitle/companyName via email signature LLM analysis |
| POST | /api/migration/post-verify | Post-migration verification (duplicates + completeness stats) |
| POST | /api/migration/archive-old-threads | Archive threads older than N days (default 90) |
| POST | /api/contacts/[id]/ai-insights | On-demand LLM analysis of contact relationship |
SECTION 17: CONTACT DETAIL TABS (COMPLETE LIST)
Row 1
| Tab | Data Source |
|---|---|
| Overview | ContactForm (editable fields) |
| Companies | ContactCompanyRole → CompanyContact |
| ContactEmail records | |
| Phone | ContactPhone records |
| Addresses | ContactAddress records |
| Social Media | ContactSocialMedia records |
Row 2
| Tab | Data Source |
|---|---|
| Activity | ContactInteraction records |
| Notes | ContactNote records |
| Tags | ContactTag → Tag |
| Calendar | CalendarEvent (via attendees) |
| Signature Intel | LLM extraction from inbound emails |
| Timeline | EmailMessage aggregation (NEW) |
| Merge Log | AuditLog with action=contact.merge (NEW) |
| Attachments | EmailAttachment with preview/fullscreen (REWRITTEN) |
| AI Insights | On-demand LLM analysis (NEW) |
| Contracts | ContractSignatory records |
End of Addendum 06
"One WEBA. Every channel. Real-time, always."
Document prepared by: WEBA Development Agent
Validated by: Alain Bessette
Date: June 6, 2026
Classification: Internal — Technical Specification