GoWeBaKnowledge Center

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

  1. Phase A — WebaBox Category Fix
  2. Phase B.1 — Migration Delete Button
  3. Phase B.2 — Google Account Disconnect
  4. Phase B.3 — Auto-Enrichment During Migration
  5. Phase B.4 — WebaCategory Classification Engine + 90-Day Archiving
  6. Phase C.1 — Contact Timeline Tab
  7. Phase C.2 — Attachments Tab (Color-Coded + Preview + Fullscreen)
  8. Phase C.3 — Merge Log Tab
  9. Phase C.4 — AI Insights Tab
  10. Phase D — Post-Migration Batch Enrichment & Verification
  11. Phase E — Multi-Contact Manager (Relational Enrichment + Merge Safety)
  12. Bug Fixes & Corrections
  13. Files Modified — Complete Inventory
  14. 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

ActionBeforeAfter
Reply to URGENT threadThread moves to "Conversations" with invalid categoryThread moves to Conversations filter (NONE + messageCount > 1)
WebaBox card countsCould show phantom countsAccurate counts across all 12 cards

1.5 Key Clarification — WebaBox 12-Card Architecture

The 12 WebaBox cards use 5 DB values + 7 smart filters:

CardSource
UrgentwebaCategory = URGENT
ImportantwebaCategory = IMPORTANT
JournalwebaCategory = JOURNAL
OfficewebaCategory = OFFICE
NonewebaCategory = NONE AND messageCount = 1
ConversationswebaCategory = NONE AND messageCount > 1
Reply LaterisReplyLater = true
Set AsideisSetAside = true
StarredisStarred = true
SpamisSpam = true
TrashdeletedAt IS NOT NULL
All EmailsAll 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:

  1. Creating/linking CompanyContact records based on company name or email domain
  2. Creating ContactCompanyRole links between Contact and CompanyContact
  3. 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:
    1. If companyName is present → find or create CompanyContact (matching by name case-insensitive OR email domain)
    2. Extract email domain, skip generic providers (gmail, yahoo, hotmail, etc.)
    3. Create ContactCompanyRole link with jobTitle as role
  • 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:
    • jobTitle
    • companyName
    • phone
    • website
    • address
    • bookingUrlNEW (Calendly, Cal.com, HubSpot meetings, Acuity, etc.)
  • Strategy: Takes last 600 chars of email body as signature block
  • Output: JSON parsed response, returns null if 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)

CategoryRegex/Rule Patterns
JOURNALDMARC reports, noreply@, no-reply@, receipts, shipping notifications, delivery confirmations, system alerts, autoresponders, bounce messages
OFFICENewsletters, social media notifications, promotional emails, "unsubscribe", Gmail category labels (CATEGORY_PROMOTIONS, CATEGORY_SOCIAL, CATEGORY_UPDATES, CATEGORY_FORUMS)
URGENTGmail starred messages, IMPORTANT label
IMPORTANTContacts known in org (email in ContactEmail table), threads with 3+ messages (active conversations)
NONEDefault 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: true on 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 endpoint
  • app/(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=preview for 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 endpoint
  • app/(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 jobTitle AND companyName by analyzing their email signatures
  • Flow:
    1. Find contacts without jobTitle/companyName (limit: 50-200 per batch)
    2. For each contact, find the latest INBOUND email (via thread link or participantEmails)
    3. Call analyzeSignatureWithLLM() on the email body
    4. Update contact fields (jobTitle, companyName, website, calendarBookingUrl)
    5. Create ContactPhone record if phone extracted
    6. 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 projectId provided, returns migration project status
  • 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:

DataSaved to (❌ Before)Tab reads from
PhoneContact.primaryPhone (text)ContactPhone (relational table)
CompanyContact.companyName (text)ContactCompanyRole → CompanyContact (relations)
Facebook/InstagramContact.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 ContactPhone for extracted phone (label: work) and mobilePhone (label: mobile)
  • Sets isPrimary: true for 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: true for 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
  • Handle extracted from URL (strips protocol/www)
  • Deduplication: checks platform:handle combination

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:

#RelationStrategy
1ContactEmailMove non-duplicate, delete dupes
2ContactPhoneMove non-duplicate, delete dupes
3ContactInteractionMove all
4ContactNoteMove all
5ContactTagMove non-duplicate, delete dupes
6ContactAddressMove all
7ContactSocialMediaMove non-duplicate, delete dupes (E.3)
8ContactCompanyRoleMove non-duplicate, delete dupes (E.2)
9ContactGroupMemberMove non-duplicate, delete dupes
10ContactObjectLinkMove (skip dupes via unique constraint)
11FileAssetRe-link all
12ClientInvoiceRe-link all
13EmailThreadRe-link all
14StudioFormSubmissionRe-link all
15ContractSignatoryRe-link all
16CalendarEventAttendeeRe-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

FilePurpose
lib/migration/weba-classify.tsWebaCategory classification engine (rules + LLM)
app/api/migration/archive-old-threads/route.ts90-day thread archiving API
app/api/migration/batch-enrich/route.tsPost-migration batch enrichment API
app/api/migration/post-verify/route.tsPost-migration verification API
app/api/contacts/[id]/ai-insights/route.tsAI Insights analysis API
app/(app)/contacts/[id]/ai-insights-tab.tsxAI Insights UI component

Modified Files

FileChanges
app/(app)/webabox/webabox-client.tsxCONVERSATIONS → NONE fix
app/(app)/migration/migration-client.tsxAdded IMPORTING/SCANNING/PLANNING to canDelete
app/api/migration/projects/[id]/route.tsAdded IMPORTING/SCANNING/PLANNING to deletableStatuses
app/api/migration/projects/[id]/execute/route.tsAdded enrichContactPostCreation + classifyWebaCategory calls
lib/migration/migration-actions.tsAdded enrichContactPostCreation() + analyzeSignatureWithLLM() + bookingUrl
app/(app)/contacts/[id]/timeline-tab.tsxFixed infinite loop (removed t from useCallback deps)
app/(app)/contacts/[id]/merge-log-tab.tsxFixed infinite loop (removed t from useCallback deps)
app/(app)/contacts/[id]/attachments-tab.tsxComplete rewrite: preview + fullscreen modal
app/(app)/contacts/[id]/contact-detail-client.tsxAdded AI Insights tab
app/api/email/extract-signature/route.tsAdded relational enrichment (ContactPhone, CompanyContact, ContactSocialMedia)
lib/weba/signature-extraction.tsReference — buildContactEnrichmentData (unchanged)
lib/contacts/contact-merge.tsE.2/E.3: dedup ContactCompanyRole + ContactSocialMedia; E.4: inherit best fields

<a id="checkpoints"></a>

SECTION 14: CHECKPOINTS TIMELINE

#CheckpointDescription
1Phase A fixURGENT → NONE (not CONVERSATIONS) in webabox-client.tsx
2Phase B.1-B.2Delete button visible + Google Disconnect (pre-existing)
3Phase C.1-C.3Timeline, Attachments (color-coded), Merge Log tabs
4Fix delete timeline mergelog previewIMPORTING delete + infinite loop fixes + Attachments preview/fullscreen
5C.4 AI Insights tab contactsOn-demand LLM analysis tab
6B.3 auto-enrich CompanyContact migrationAuto-enrichment during migration execution
7B.4 WebaCategory classify archive 90jClassification engine + archiving API
8D batch-enrich post-verify APIsBatch enrichment + verification endpoints
9Fix delete API allow IMPORTING statusBackend API fix for IMPORTING deletion
10Add bookingUrl extraction from signaturesBookingUrl added to LLM signature extraction
11E multi-contact relational enrich mergeRelational 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, primaryEmail
  • ContactPhone — phone, label, isPrimary
  • ContactEmail — email, isPrimary
  • ContactCompanyRole — contactId, companyContactId, role, title, isPrimary
  • CompanyContact — name, domain, website, organizationId
  • ContactSocialMedia — platform (SocialPlatform enum), handle, url, isPrimary
  • ContactAddress — full address fields
  • EmailThread — webaCategory (WebaCategory enum), isArchived
  • ContactInteraction — subject, body
  • ContactNote — content

No prisma db push required. No --force-reset. No --accept-data-loss.


SECTION 16: API ENDPOINTS CREATED

MethodEndpointPurpose
POST/api/migration/batch-enrichBatch-enrich contacts missing jobTitle/companyName via email signature LLM analysis
POST/api/migration/post-verifyPost-migration verification (duplicates + completeness stats)
POST/api/migration/archive-old-threadsArchive threads older than N days (default 90)
POST/api/contacts/[id]/ai-insightsOn-demand LLM analysis of contact relationship

SECTION 17: CONTACT DETAIL TABS (COMPLETE LIST)

Row 1

TabData Source
OverviewContactForm (editable fields)
CompaniesContactCompanyRole → CompanyContact
EmailContactEmail records
PhoneContactPhone records
AddressesContactAddress records
Social MediaContactSocialMedia records

Row 2

TabData Source
ActivityContactInteraction records
NotesContactNote records
TagsContactTag → Tag
CalendarCalendarEvent (via attendees)
Signature IntelLLM extraction from inbound emails
TimelineEmailMessage aggregation (NEW)
Merge LogAuditLog with action=contact.merge (NEW)
AttachmentsEmailAttachment with preview/fullscreen (REWRITTEN)
AI InsightsOn-demand LLM analysis (NEW)
ContractsContractSignatory 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

© 2024–2026 GOWEBA INC. — Make it Simple, Make it Possible, Make it Real.