8- * DUAL-WRITE MODE:
9- * When DUAL_WRITE_ENABLED is true, all write operations (insert, update, delete)
10- * are automatically mirrored to MongoDB via MongoDatabase class.
11- * This allows gradual migration from MySQL to MongoDB.
12- *
13: * To disable MySQL later, set MYSQL_ENABLED=false in config.php
14- *
15- * CALLING MAP:
16- * - Called by: EmailSync.php, AIAnalyzer.php, ContactProfiler.php
17- * - Called by: All CLI scripts, All API endpoints
18- * - Uses: config/config.php for credentials
19- * - Uses: MongoDatabase.php for dual-write
20- *
21- * USAGE:
22- *   $db = Database::getInstance();
23: *   $contacts = $db->fetchAll("SELECT * FROM contacts WHERE type = ?", ['client']);
24- *   $db->insert('contacts', ['email' => 'test@test.com', 'name' => 'Test']);
25- *
26- * @package EmailIntelligence
27- */
28-
--
80-     *
81-     * @throws Exception If connection fails
82-     */
83-    private function __construct()
84-    {
85:        $this->mysqlEnabled = !defined('MYSQL_ENABLED') || MYSQL_ENABLED;
86-        $this->dualWriteEnabled = defined('DUAL_WRITE_ENABLED') && DUAL_WRITE_ENABLED;
87-
88-        if ($this->mysqlEnabled) {
89-            $this->connect();
90-        }
--
159-     * @param string $sql SQL query with placeholders
160-     * @param array $params Parameters to bind
161-     * @return array Array of associative arrays
162-     * @throws Exception On query error
163-     */
164:    public function fetchAll(string $sql, array $params = []): array
165-    {
166-        // If MySQL is disabled, try MongoDB
167-        if (!$this->mysqlEnabled) {
168-            if (!$this->mongo) {
169-                throw new Exception("Neither MySQL nor MongoDB is available");
--
176-        }
177-
178-        try {
179-            $stmt = $this->pdo->prepare($sql);
180-            $stmt->execute($params);
181:            return $stmt->fetchAll();
182-        } catch (PDOException $e) {
183-            $this->logError("Query failed: {$sql} - " . $e->getMessage());
184-            throw new Exception("Query failed: " . $e->getMessage());
185-        }
186-    }
--
191-     * @param string $sql SQL query with placeholders
192-     * @param array $params Parameters to bind
193-     * @return array|null Single row or null if not found
194-     * @throws Exception On query error
195-     */
196:    public function fetchOne(string $sql, array $params = []): ?array
197-    {
198-        // If MySQL is disabled, try MongoDB
199-        if (!$this->mysqlEnabled) {
200-            if (!$this->mongo) {
201-                throw new Exception("Neither MySQL nor MongoDB is available");
--
893-
894-        // Test MySQL
895-        if ($this->mysqlEnabled && $this->pdo) {
896-            try {
897-                $version = $this->fetchColumn("SELECT VERSION()");
898:                $tables = $this->fetchAll("SHOW TABLES");
899-
900-                $result['mysql'] = [
901-                    'connected' => true,
902-                    'version' => $version,
903-                    'database' => DB_NAME,


   164→    public function fetchAll(string $sql, array $params = []): array
   165→    {
   166→        // If MySQL is disabled, try MongoDB
   167→        if (!$this->mysqlEnabled) {
   168→            if (!$this->mongo) {
   169→                throw new Exception("Neither MySQL nor MongoDB is available");
   170→            }
   171→            return $this->mongoFetchAll($sql, $params);
   172→        }
   173→
   174→        if (!$this->pdo) {
   175→            throw new Exception("MySQL connection not available");
   176→        }
   177→
   178→        try {
   179→            $stmt = $this->pdo->prepare($sql);
   180→            $stmt->execute($params);
   181→            return $stmt->fetchAll();
   182→        } catch (PDOException $e) {
   183→            $this->logError("Query failed: {$sql} - " . $e->getMessage());
   184→            throw new Exception("Query failed: " . $e->getMessage());
   185→        }
   186→    }
   187→
   188→    /**
   189→     * Execute a query and return single row
   190→     *
   191→     * @param string $sql SQL query with placeholders
   192→     * @param array $params Parameters to bind
   193→     * @return array|null Single row or null if not found

    public function fetchAll(string $sql, array $params = []): array
    {
        // If MySQL is disabled, try MongoDB
        if (!$this->mysqlEnabled) {
            if (!$this->mongo) {
                throw new Exception("Neither MySQL nor MongoDB is available");
            }
            return $this->mongoFetchAll($sql, $params);
        }

        if (!$this->pdo) {
            throw new Exception("MySQL connection not available");
        }

        try {
            $stmt = $this->pdo->prepare($sql);
            $stmt->execute($params);
            return $stmt->fetchAll();
        } catch (PDOException $e) {
            $this->logError("Query failed: {$sql} - " . $e->getMessage());
            throw new Exception("Query failed: " . $e->getMessage());
        }
    }

    /**
     * Execute a query and return single row
     *
     * @param string $sql SQL query with placeholders
     * @param array $params Parameters to bind
     * @return array|null Single row or null if not found


So when MySQL is disabled, it uses `mongoFetchAll()` which translates SQL to MongoDB queries. The issue is that the MongoDB document for the newly added Gmail account probably doesn't have a `last_sync_at` field, and when `getStats()` accesses `$account['last_sync_at']`, the error handler converts the "Undefined index" notice to an exception.

Let me look at the `mongoFetchAll` to understand what it returns, and then fix the `getStats()` method to use null-coalescing.


   329→    public function getStats(): array
   330→    {
   331→        $accounts = $this->getAll(false);
   332→        $stats = [
   333→            'total_accounts' => count($accounts),
   334→            'active_accounts' => 0,
   335→            'authenticated_accounts' => 0,
   336→            'total_emails' => 0,
   337→            'accounts' => []
   338→        ];
   339→
   340→        foreach ($accounts as $account) {
   341→            $emailCount = $this->db->fetchColumn(
   342→                "SELECT COUNT(*) FROM emails WHERE gmail_account_id = ?",
   343→                [$account['id']]
   344→            );
   345→
   346→            $accountStats = [
   347→                'id' => $account['id'],
   348→                'email' => $account['email'],
   349→                'name' => $account['name'],
   350→                'is_primary' => (bool)$account['is_primary'],
   351→                'is_active' => (bool)$account['is_active'],
   352→                'is_authenticated' => $this->hasValidToken($account),
   353→                'email_count' => (int)$emailCount,
   354→                'last_sync' => $account['last_sync_at'],
   355→                'sync_status' => $account['sync_status']
   356→            ];
   357→
   358→            $stats['accounts'][] = $accountStats;
   359→
   360→            if ($account['is_active']) {
   361→                $stats['active_accounts']++;
   362→            }
   363→            if ($accountStats['is_authenticated']) {
   364→                $stats['authenticated_accounts']++;
   365→            }
   366→            $stats['total_emails'] += $emailCount;
   367→        }
   368→
   369→        return $stats;
   370→    }
   371→}
   372→
   373→

    public function getStats(): array
    {
        $accounts = $this->getAll(false);
        $stats = [
            'total_accounts' => count($accounts),
            'active_accounts' => 0,
            'authenticated_accounts' => 0,
            'total_emails' => 0,
            'accounts' => []
        ];

        foreach ($accounts as $account) {
            $emailCount = $this->db->fetchColumn(
                "SELECT COUNT(*) FROM emails WHERE gmail_account_id = ?",
                [$account['id']]
            );

            $accountStats = [
                'id' => $account['id'],
                'email' => $account['email'],
                'name' => $account['name'],
                'is_primary' => (bool)$account['is_primary'],
                'is_active' => (bool)$account['is_active'],
                'is_authenticated' => $this->hasValidToken($account),
                'email_count' => (int)$emailCount,
                'last_sync' => $account['last_sync_at'],
                'sync_status' => $account['sync_status']
            ];

            $stats['accounts'][] = $accountStats;

            if ($account['is_active']) {
                $stats['active_accounts']++;
            }
            if ($accountStats['is_authenticated']) {
                $stats['authenticated_accounts']++;
            }
            $stats['total_emails'] += $emailCount;
        }

        return $stats;
    }
}


Found it. Lines 354-355 access `$account['last_sync_at']` and `$account['sync_status']` without null-coalescing. When MySQL is disabled and MongoDB is used, new documents may not have these fields. Also line 350 `$account['is_primary']` and others could be missing.

Now let me also check the `GmailClient` constructor to answer the second question about multi-account search.


     1→<?php
     2→/**
     3→ * Gmail API Client
     4→ *
     5→ * Handles OAuth2 authentication and Gmail API interactions
     6→ * for fetching emails from Gmail accounts.
     7→ *
     8→ * MULTI-ACCOUNT SUPPORT:
     9→ * - Can be instantiated with a specific account ID or email
    10→ * - Falls back to primary account if none specified
    11→ * - Each account has its own token file
    12→ *
    13→ * CALLING MAP:
    14→ * - Called by: EmailSync.php (main consumer)
    15→ * - Called by: public/oauth_callback.php (OAuth flow)
    16→ * - Called by: cli/sync_emails.php
    17→ * - Uses: config/config.php for OAuth credentials
    18→ * - Uses: GmailAccount.php for multi-account management
    19→ * - Uses: config/gmail_token_*.json for stored tokens
    20→ *
    21→ * OAUTH FLOW:
    22→ * 1. User visits public/index.php -> clicks "Connect Gmail"
    23→ * 2. Redirected to Google OAuth consent screen (with account state)
    24→ * 3. Google redirects to public/oauth_callback.php with code
    25→ * 4. oauth_callback.php calls handleAuthCallback() to exchange code for token
    26→ * 5. Token stored in config/gmail_token_{email}.json
    27→ * 6. Subsequent calls use stored token, auto-refreshing when expired
    28→ *
    29→ * @package EmailIntelligence
    30→ */
    31→
    32→namespace EmailIntelligence;
    33→
    34→use Google\Client as GoogleClient;
    35→use Google\Service\Gmail;
    36→use Google\Service\Gmail\Message;
    37→use Exception;
    38→
    39→class GmailClient
    40→{
    41→    /**
    42→     * Google API Client instance
    43→     * @var GoogleClient
    44→     */
    45→    private GoogleClient $client;
    46→
    47→    /**
    48→     * Gmail Service instance
    49→     * @var Gmail|null
    50→     */
    51→    private ?Gmail $service = null;
    52→
    53→    /**
    54→     * Current account data
    55→     * @var array|null
    56→     */
    57→    private ?array $account = null;
    58→
    59→    /**
    60→     * Account ID (from gmail_accounts table)
    61→     * @var int|null
    62→     */
    63→    private ?int $accountId = null;
    64→
    65→    /**
    66→     * User's email address
    67→     * @var string
    68→     */
    69→    private string $userEmail;
    70→
    71→    /**
    72→     * Path to OAuth credentials JSON
    73→     * @var string
    74→     */
    75→    private string $credentialsPath;
    76→
    77→    /**
    78→     * Path to store OAuth token
    79→     * @var string
    80→     */

<?php
/**
 * Gmail API Client
 *
 * Handles OAuth2 authentication and Gmail API interactions
 * for fetching emails from Gmail accounts.
 *
 * MULTI-ACCOUNT SUPPORT:
 * - Can be instantiated with a specific account ID or email
 * - Falls back to primary account if none specified
 * - Each account has its own token file
 *
 * CALLING MAP:
 * - Called by: EmailSync.php (main consumer)
 * - Called by: public/oauth_callback.php (OAuth flow)
 * - Called by: cli/sync_emails.php
 * - Uses: config/config.php for OAuth credentials
 * - Uses: GmailAccount.php for multi-account management
 * - Uses: config/gmail_token_*.json for stored tokens
 *
 * OAUTH FLOW:
 * 1. User visits public/index.php -> clicks "Connect Gmail"
 * 2. Redirected to Google OAuth consent screen (with account state)
 * 3. Google redirects to public/oauth_callback.php with code
 * 4. oauth_callback.php calls handleAuthCallback() to exchange code for token
 * 5. Token stored in config/gmail_token_{email}.json
 * 6. Subsequent calls use stored token, auto-refreshing when expired
 *
 * @package EmailIntelligence
 */

namespace EmailIntelligence;

use Google\Client as GoogleClient;
use Google\Service\Gmail;
use Google\Service\Gmail\Message;
use Exception;

class GmailClient
{
    /**
     * Google API Client instance
     * @var GoogleClient
     */
    private GoogleClient $client;

    /**
     * Gmail Service instance
     * @var Gmail|null
     */
    private ?Gmail $service = null;

    /**
     * Current account data
     * @var array|null
     */
    private ?array $account = null;

    /**
     * Account ID (from gmail_accounts table)
     * @var int|null
     */
    private ?int $accountId = null;

    /**
     * User's email address
     * @var string
     */
    private string $userEmail;

    /**
     * Path to OAuth credentials JSON
     * @var string
     */
    private string $credentialsPath;

    /**
     * Path to store OAuth token
     * @var string
     */


    80→     */
    81→    private string $tokenPath;
    82→
    83→    /**
    84→     * Whether client is authenticated
    85→     * @var bool
    86→     */
    87→    private bool $isAuthenticated = false;
    88→
    89→    /**
    90→     * Flag to prevent infinite recursion during account loading
    91→     * @var bool
    92→     */
    93→    private static bool $loadingAccount = false;
    94→
    95→    /**
    96→     * Constructor - supports multi-account
    97→     *
    98→     * @param int|string|null $account Account ID, email address, or null for primary
    99→     * @throws Exception If OAuth credentials file not found
   100→     */
   101→    public function __construct($account = null)
   102→    {
   103→        $this->credentialsPath = GMAIL_OAUTH_CREDENTIALS;
   104→
   105→        // Load account from database if multi-account is enabled
   106→        $this->loadAccount($account);
   107→
   108→        $this->initializeClient();
   109→    }
   110→
   111→    /**
   112→     * Load account from database or use legacy config
   113→     *
   114→     * @param int|string|null $account Account ID, email, or null for primary
   115→     */
   116→    private function loadAccount($account): void
   117→    {
   118→        // Prevent infinite recursion - check if we're already loading
   119→        if (self::$loadingAccount) {
   120→            $this->useLegacyConfig();
   121→            return;
   122→        }
   123→
   124→        self::$loadingAccount = true;
   125→        try {
   126→            // Check if gmail_accounts table exists first to avoid errors
   127→            $db = Database::getInstance();
   128→            $tableExists = $db->fetchColumn(
   129→                "SELECT COUNT(*) FROM information_schema.tables
   130→                 WHERE table_schema = DATABASE() AND table_name = 'gmail_accounts'"
   131→            );
   132→
   133→            if (!$tableExists) {
   134→                // Table doesn't exist, use legacy config
   135→                $this->useLegacyConfig();
   136→                self::$loadingAccount = false;
   137→                return;
   138→            }
   139→
   140→            $accountManager = new GmailAccount();
   141→
   142→            if ($account === null) {
   143→                // Get primary account
   144→                $this->account = $accountManager->getPrimary();
   145→            } elseif (is_int($account)) {
   146→                // Get by ID
   147→                $this->account = $accountManager->getById($account);
   148→            } elseif (is_string($account) && strpos($account, '@') !== false) {
   149→                // Get by email

     */
    private string $tokenPath;

    /**
     * Whether client is authenticated
     * @var bool
     */
    private bool $isAuthenticated = false;

    /**
     * Flag to prevent infinite recursion during account loading
     * @var bool
     */
    private static bool $loadingAccount = false;

    /**
     * Constructor - supports multi-account
     *
     * @param int|string|null $account Account ID, email address, or null for primary
     * @throws Exception If OAuth credentials file not found
     */
    public function __construct($account = null)
    {
        $this->credentialsPath = GMAIL_OAUTH_CREDENTIALS;

        // Load account from database if multi-account is enabled
        $this->loadAccount($account);

        $this->initializeClient();
    }

    /**
     * Load account from database or use legacy config
     *
     * @param int|string|null $account Account ID, email, or null for primary
     */
    private function loadAccount($account): void
    {
        // Prevent infinite recursion - check if we're already loading
        if (self::$loadingAccount) {
            $this->useLegacyConfig();
            return;
        }

        self::$loadingAccount = true;
        try {
            // Check if gmail_accounts table exists first to avoid errors
            $db = Database::getInstance();
            $tableExists = $db->fetchColumn(
                "SELECT COUNT(*) FROM information_schema.tables
                 WHERE table_schema = DATABASE() AND table_name = 'gmail_accounts'"
            );

            if (!$tableExists) {
                // Table doesn't exist, use legacy config
                $this->useLegacyConfig();
                self::$loadingAccount = false;
                return;
            }

            $accountManager = new GmailAccount();

            if ($account === null) {
                // Get primary account
                $this->account = $accountManager->getPrimary();
            } elseif (is_int($account)) {
                // Get by ID
                $this->account = $accountManager->getById($account);
            } elseif (is_string($account) && strpos($account, '@') !== false) {
                // Get by email


Now I see the picture. `GmailClient()` without params gets the primary account. The `search_gmail_threads` action at line 5761 does `$gmail = new \EmailIntelligence\GmailClient()` — this only uses the **primary** account. To search all connected accounts, it would need to loop through all accounts.

Let me fix both issues now.


TodoWrite

Fix GmailAccount::getStats() - null-coalesce missing fields

Fixing GmailAccount::getStats() undefined index error

Update search_gmail_threads to search all connected accounts

Updating search_gmail_threads to search all accounts

Update import_gmail_threads to use correct account

Updating import_gmail_threads for multi-account

Verify with php -l syntax check


Todos have been modified successfully. Ensure that you continue to use the todo list to track your progress. Please proceed with the current tasks if applicable

Fix GmailAccount::getStats() - null-coalesce missing fields

Fixing GmailAccount::getStats() undefined index error

Update search_gmail_threads to search all connected accounts

Updating search_gmail_threads to search all accounts

Update import_gmail_threads to use correct account

Updating import_gmail_threads for multi-account

Verify with php -l syntax check


**Fix 1:** The `Undefined index: last_sync_at` error in `GmailAccount::getStats()`.


Edit

            $accountStats = [
                'id' => $account['id'],
                'email' => $account['email'],
                'name' => $account['name'],
                'is_primary' => (bool)$account['is_primary'],
                'is_active' => (bool)$account['is_active'],
                'is_authenticated' => $this->hasValidToken($account),
                'email_count' => (int)$emailCount,
                'last_sync' => $account['last_sync_at'],
                'sync_status' => $account['sync_status']
            ];

            $accountStats = [
                'id' => $account['id'],
                'email' => $account['email'],
                'name' => $account['name'] ?? '',
                'is_primary' => (bool)($account['is_primary'] ?? false),
                'is_active' => (bool)($account['is_active'] ?? true),
                'is_authenticated' => $this->hasValidToken($account),
                'email_count' => (int)$emailCount,
                'last_sync' => $account['last_sync_at'] ?? null,
                'sync_status' => $account['sync_status'] ?? 'idle'
            ];


The file /var/www/mssql.danmarcrm.com/dev1/extension2v1/src/GmailAccount.php has been updated successfully.

            $accountStats = [
                'id' => $account['id'],
                'email' => $account['email'],
                'name' => $account['name'] ?? '',
                'is_primary' => (bool)($account['is_primary'] ?? false),
                'is_active' => (bool)($account['is_active'] ?? true),
                'is_authenticated' => $this->hasValidToken($account),
                'email_count' => (int)$emailCount,
                'last_sync' => $account['last_sync_at'] ?? null,
                'sync_status' => $account['sync_status'] ?? 'idle'
            ];

<?php
/**
 * Gmail Account Manager
 *
 * Manages multiple Gmail accounts for the Email Intelligence system.
 * Handles account CRUD operations and provides account selection.
 *
 * CALLING MAP:
 * - Called by: GmailClient.php for account selection
 * - Called by: EmailSync.php for multi-account sync
 * - Called by: API endpoints for account management
 * - Uses: Database.php for persistence
 *
 * @package EmailIntelligence
 */

namespace EmailIntelligence;

use Exception;

class GmailAccount
{
    private Database $db;
    private string $configPath;

    /**
     * Constructor
     */
    public function __construct()
    {
        $this->db = Database::getInstance();
        $this->configPath = defined('CONFIG_PATH') ? CONFIG_PATH : dirname(__DIR__) . '/config';
    }

    /**
     * Get all Gmail accounts
     *
     * @param bool $activeOnly Only return active accounts
     * @return array
     */
    public function getAll(bool $activeOnly = true): array
    {
        $sql = "SELECT * FROM gmail_accounts";
        if ($activeOnly) {
            $sql .= " WHERE is_active = 1";
        }
        $sql .= " ORDER BY is_primary DESC, email ASC";

        return $this->db->fetchAll($sql);
    }

    /**
     * Get account by ID
     *
     * @param int $id Account ID
     * @return array|null
     */
    public function getById(int $id): ?array
    {
        return $this->db->fetchOne(
            "SELECT * FROM gmail_accounts WHERE id = ?",
            [$id]
        );
    }

    /**
     * Get account by email
     *
     * @param string $email Email address
     * @return array|null
     */
    public function getByEmail(string $email): ?array
    {
        return $this->db->fetchOne(
            "SELECT * FROM gmail_accounts WHERE email = ?",
            [strtolower($email)]
        );
    }

    /**
     * Get primary account
     *
     * @return array|null
     */
    public function getPrimary(): ?array
    {
        return $this->db->fetchOne(
            "SELECT * FROM gmail_accounts WHERE is_primary = 1 AND is_active = 1 LIMIT 1"
        );
    }

    /**
     * Create a new Gmail account entry
     *
     * @param string $email Email address
     * @param string $name Display name (optional)
     * @param bool $isPrimary Set as primary account
     * @return int Account ID
     * @throws Exception If email already exists
     */
    public function create(string $email, string $name = '', bool $isPrimary = false): int
    {
        $email = strtolower(trim($email));

        // Check if already exists
        if ($this->getByEmail($email)) {
            throw new Exception("Gmail account {$email} already exists");
        }

        // Generate unique token filename
        $tokenFile = $this->generateTokenFilename($email);

        // If this will be primary, unset other primaries
        if ($isPrimary) {
            $this->db->execute("UPDATE gmail_accounts SET is_primary = 0");
        }

        // If no accounts exist, make this primary
        $accountCount = $this->db->fetchColumn("SELECT COUNT(*) FROM gmail_accounts");
        if ($accountCount == 0) {
            $isPrimary = true;
        }

        $id = $this->db->insert('gmail_accounts', [
            'email' => $email,
            'name' => $name ?: $this->extractNameFromEmail($email),
            'token_file' => $tokenFile,
            'is_primary' => $isPrimary ? 1 : 0,
            'is_active' => 1
        ]);

        return $id;
    }

    /**
     * Update account details
     *
     * @param int $id Account ID
     * @param array $data Data to update
     * @return bool
     */
    public function update(int $id, array $data): bool
    {
        $allowedFields = ['name', 'is_active', 'is_primary', 'last_sync_at',
                         'last_history_id', 'sync_status', 'last_error', 'total_emails_synced'];

        $updateData = array_intersect_key($data, array_flip($allowedFields));

        if (empty($updateData)) {
            return false;
        }

        // If setting as primary, unset other primaries
        if (isset($updateData['is_primary']) && $updateData['is_primary']) {
            $this->db->execute("UPDATE gmail_accounts SET is_primary = 0 WHERE id != ?", [$id]);
        }

        return $this->db->update('gmail_accounts', $updateData, 'id = ?', [$id]) > 0;
    }

    /**
     * Delete an account
     *
     * @param int $id Account ID
     * @param bool $deleteToken Also delete the token file
     * @return bool
     */
    public function delete(int $id, bool $deleteToken = true): bool
    {
        $account = $this->getById($id);
        if (!$account) {
            return false;
        }

        // Don't delete primary account if it's the only one
        if ($account['is_primary']) {
            $otherAccounts = $this->db->fetchColumn(
                "SELECT COUNT(*) FROM gmail_accounts WHERE id != ? AND is_active = 1",
                [$id]
            );
            if ($otherAccounts == 0) {
                throw new Exception("Cannot delete the only active account. Add another account first.");
            }
        }

        // Delete token file if requested
        if ($deleteToken && $account['token_file']) {
            $tokenPath = $this->configPath . '/' . $account['token_file'];
            if (file_exists($tokenPath)) {
                unlink($tokenPath);
            }
        }

        // Nullify references in emails table (don't delete emails)
        $this->db->execute(
            "UPDATE emails SET gmail_account_id = NULL WHERE gmail_account_id = ?",
            [$id]
        );

        return $this->db->delete('gmail_accounts', 'id = ?', [$id]) > 0;
    }

    /**
     * Get full token file path for an account
     *
     * @param int|array $account Account ID or account array
     * @return string
     */
    public function getTokenPath($account): string
    {
        if (is_int($account)) {
            $account = $this->getById($account);
        }

        if (!$account || empty($account['token_file'])) {
            throw new Exception("Invalid account or missing token file");
        }

        return $this->configPath . '/' . $account['token_file'];
    }

    /**
     * Check if account has valid token
     *
     * @param int|array $account Account ID or account array
     * @return bool
     */
    public function hasValidToken($account): bool
    {
        try {
            $tokenPath = $this->getTokenPath($account);
            if (!file_exists($tokenPath)) {
                return false;
            }

            $token = json_decode(file_get_contents($tokenPath), true);
            return isset($token['access_token']);
        } catch (Exception $e) {
            return false;
        }
    }

    /**
     * Update sync status for an account
     *
     * @param int $accountId Account ID
     * @param string $status 'idle', 'running', 'error'
     * @param string|null $error Error message if status is error
     * @param string|null $historyId Gmail history ID
     * @param int $emailsSynced Number of emails synced
     */
    public function updateSyncStatus(int $accountId, string $status, ?string $error = null,
                                     ?string $historyId = null, int $emailsSynced = 0): void
    {
        $data = [
            'sync_status' => $status,
            'last_error' => $error
        ];

        if ($status === 'idle' || $status === 'error') {
            $data['last_sync_at'] = date('Y-m-d H:i:s');
        }

        if ($historyId) {
            $data['last_history_id'] = $historyId;
        }

        if ($emailsSynced > 0) {
            $this->db->execute(
                "UPDATE gmail_accounts SET total_emails_synced = total_emails_synced + ? WHERE id = ?",
                [$emailsSynced, $accountId]
            );
        }

        $this->update($accountId, $data);
    }

    /**
     * Get accounts that need sync (active with valid tokens)
     *
     * @return array
     */
    public function getAccountsForSync(): array
    {
        $accounts = $this->getAll(true);

        return array_filter($accounts, function($account) {
            return $this->hasValidToken($account);
        });
    }

    /**
     * Generate unique token filename from email
     *
     * @param string $email
     * @return string
     */
    private function generateTokenFilename(string $email): string
    {
        // Clean email for filename
        $clean = preg_replace('/[^a-z0-9]/', '_', strtolower($email));
        $clean = preg_replace('/_+/', '_', $clean);
        $clean = trim($clean, '_');

        return "gmail_token_{$clean}.json";
    }

    /**
     * Extract display name from email
     *
     * @param string $email
     * @return string
     */
    private function extractNameFromEmail(string $email): string
    {
        $parts = explode('@', $email);
        $local = $parts[0];

        // Convert dots/underscores to spaces and capitalize
        $name = str_replace(['.', '_'], ' ', $local);
        return ucwords($name);
    }

    /**
     * Get statistics for all accounts
     *
     * @return array
     */
    public function getStats(): array
    {
        $accounts = $this->getAll(false);
        $stats = [
            'total_accounts' => count($accounts),
            'active_accounts' => 0,
            'authenticated_accounts' => 0,
            'total_emails' => 0,
            'accounts' => []
        ];

        foreach ($accounts as $account) {
            $emailCount = $this->db->fetchColumn(
                "SELECT COUNT(*) FROM emails WHERE gmail_account_id = ?",
                [$account['id']]
            );

            $accountStats = [
                'id' => $account['id'],
                'email' => $account['email'],
                'name' => $account['name'],
                'is_primary' => (bool)$account['is_primary'],
                'is_active' => (bool)$account['is_active'],
                'is_authenticated' => $this->hasValidToken($account),
                'email_count' => (int)$emailCount,
                'last_sync' => $account['last_sync_at'],
                'sync_status' => $account['sync_status']
            ];

            $stats['accounts'][] = $accountStats;

            if ($account['is_active']) {
                $stats['active_accounts']++;
            }
            if ($accountStats['is_authenticated']) {
                $stats['authenticated_accounts']++;
            }
            $stats['total_emails'] += $emailCount;
        }

        return $stats;
    }
}

-                'name' => $account['name'],

-                'is_primary' => (bool)$account['is_primary'],

-                'is_active' => (bool)$account['is_active'],

+                'name' => $account['name'] ?? '',

+                'is_primary' => (bool)($account['is_primary'] ?? false),

+                'is_active' => (bool)($account['is_active'] ?? true),

-                'last_sync' => $account['last_sync_at'],

-                'sync_status' => $account['sync_status']

+                'last_sync' => $account['last_sync_at'] ?? null,

+                'sync_status' => $account['sync_status'] ?? 'idle'


Also fix the `index.php` line 110 that accesses `$syncStatus['last_sync_at']` directly.


100→                    <a href="<?= htmlspecialchars($authUrl) ?>" class="btn btn-primary">Connect Gmail
   101→                    <?php endif; ?>
   102→                <?php endif; ?>
   103→

   104→
   105→
   106→

   107→
Sync Status

   108→
<?= htmlspecialchars($syncStatus['status'] ?? 'unknown') ?>

   109→

   110→                    Last: <?= $syncStatus['last_sync_at'] ? date('M j, H:i', strtotime($syncStatus['last_sync_at'])) : 'Never' ?>
   111→

   112→                <?php if ($gmailStatus['isAuthenticated']): ?>
   113→                <button class="btn btn-small" onclick="triggerSync()">Sync Now</button>
   114→                <?php endif; ?>
   115→

   116→
   117→
   118→

   119→
Emails

   120→
<?= number_format($stats['emails']) ?>

   121→

   122→                    <?= number_format($stats['analyzed']) ?> analyzed,
   123→                    <?= number_format($stats['pending']) ?> pending
   124→

<a href="<?= htmlspecialchars($authUrl) ?>" class="btn btn-primary">Connect Gmail
                    <?php endif; ?>
                <?php endif; ?>

Sync Status

<?= htmlspecialchars($syncStatus['status'] ?? 'unknown') ?>

                    Last: <?= $syncStatus['last_sync_at'] ? date('M j, H:i', strtotime($syncStatus['last_sync_at'])) : 'Never' ?>

                <?php if ($gmailStatus['isAuthenticated']): ?>
                <button class="btn btn-small" onclick="triggerSync()">Sync Now</button>
                <?php endif; ?>

Emails

<?= number_format($stats['emails']) ?>

                    <?= number_format($stats['analyzed']) ?> analyzed,
                    <?= number_format($stats['pending']) ?> pending


Last: <?= $syncStatus['last_sync_at'] ? date('M j, H:i', strtotime($syncStatus['last_sync_at'])) : 'Never' ?>

Last: <?= !empty($syncStatus['last_sync_at']) ? date('M j, H:i', strtotime($syncStatus['last_sync_at'])) : 'Never' ?>


The file /var/www/mssql.danmarcrm.com/dev1/extension2v1/public/index.php has been updated successfully.

<?php
/**
 * Email Intelligence Dashboard
 *
 * Main web interface for viewing contacts, emails, and analysis results.
 *
 * CALLING MAP:
 * - Entry point: Web browser
 * - Uses: api.php for data (via AJAX)
 * - Uses: oauth_callback.php for Gmail connection
 *
 * @package EmailIntelligence
 */

// Load configuration
require_once dirname(__DIR__) . '/config/config.php';
require_once dirname(__DIR__) . '/vendor/autoload.php';

use EmailIntelligence\Database;
use EmailIntelligence\GmailClient;

// Start session for flash messages
session_start();

// If contact parameter is present, redirect to contact view
if (isset($_GET['contact']) && is_numeric($_GET['contact'])) {
    header('Location: contact_view.php?contact=' . (int)$_GET['contact']);
    exit;
}

// Get flash message if any
$flashMessage = $_SESSION['flash_message'] ?? null;
unset($_SESSION['flash_message']);

// Get initial status
try {
    $db = Database::getInstance();
    $syncStatus = $db->fetchOne("SELECT * FROM sync_status WHERE id = 1");
    $stats = [
        'emails' => $db->fetchColumn("SELECT COUNT(*) FROM emails"),
        'contacts' => $db->fetchColumn("SELECT COUNT(*) FROM contacts"),
        'analyzed' => $db->fetchColumn("SELECT COUNT(*) FROM emails WHERE is_analyzed = 1"),
        'pending' => $db->fetchColumn("SELECT COUNT(*) FROM emails WHERE is_analyzed = 0"),
    ];

    $gmail = new GmailClient();
    $gmailStatus = $gmail->getAuthStatus();
    $authUrl = $gmail->hasCredentials() ? $gmail->getAuthUrl() : null;

} catch (Exception $e) {
    $error = $e->getMessage();
    $stats = ['emails' => 0, 'contacts' => 0, 'analyzed' => 0, 'pending' => 0];
    $gmailStatus = ['isAuthenticated' => false];
    $syncStatus = null;
    $authUrl = null;
}
?>
<!DOCTYPE html>

    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Email Intelligence Dashboard</title>
    <link rel="stylesheet" href="assets/style.css">

Email Intelligence

AI-Powered Email Analysis for <?= htmlspecialchars(GMAIL_USER_EMAIL) ?>

        <?php if (isset($error)): ?>

            Error: <?= htmlspecialchars($error) ?>

        <?php endif; ?>

        <?php if ($flashMessage): ?>
        <div class="alert alert-<?= $flashMessage['type'] ?>">
            <?= htmlspecialchars($flashMessage['text']) ?>

        <?php endif; ?>

            <div class="status-card <?= $gmailStatus['isAuthenticated'] ? 'status-ok' : 'status-warning' ?>">

Gmail

                <?php if ($gmailStatus['isAuthenticated']): ?>

Connected

<?= htmlspecialchars($gmailStatus['userEmail']) ?>

                <?php elseif (!$gmailStatus['hasCredentials']): ?>

Not Configured

OAuth credentials missing

                <?php else: ?>

Not Connected

                    <?php if ($authUrl): ?>
                    <a href="<?= htmlspecialchars($authUrl) ?>" class="btn btn-primary">Connect Gmail
                    <?php endif; ?>
                <?php endif; ?>

Sync Status

<?= htmlspecialchars($syncStatus['status'] ?? 'unknown') ?>

                    Last: <?= $syncStatus['last_sync_at'] ? date('M j, H:i', strtotime($syncStatus['last_sync_at'])) : 'Never' ?>

                <?php if ($gmailStatus['isAuthenticated']): ?>
                <button class="btn btn-small" onclick="triggerSync()">Sync Now</button>
                <?php endif; ?>

Emails

<?= number_format($stats['emails']) ?>

                    <?= number_format($stats['analyzed']) ?> analyzed,
                    <?= number_format($stats['pending']) ?> pending

Contacts

<?= number_format($stats['contacts']) ?>

Clients & Suppliers

AI Thread Analysis

                    Threads
                    -

                    Analyzed
                    -

                    Pending
                    -

                    Progress
                    -

                <button class="btn btn-primary" onclick="triggerAnalysis()" id="btn-analyze">Analyze Threads</button>
                <button class="btn btn-small" onclick="refreshAnalysisStatus()">Refresh Status</button>

            <button class="tab-btn active" data-tab="contacts">Contacts</button>
            <button class="tab-btn" data-tab="emails">Threads</button>

            <button class="tab-btn" data-tab="gmail-accounts">📧 Gmail Accounts</button>
            <button class="tab-btn" data-tab="enrichment">🔬 Enrichment</button>
            <button class="tab-btn" data-tab="patterns">📊 Patterns</button>
            <button class="tab-btn" data-tab="business">💼 Business</button>
            <button class="tab-btn" data-tab="alerts">⚠ Alerts</button>
            <button class="tab-btn" data-tab="search">🔍 AI Search</button>
            <button class="tab-btn" data-tab="quotes">Quotes</button>
            <button class="tab-btn" data-tab="filters">Filters</button>
            <button class="tab-btn" data-tab="business-ideas">💡 Business Ideas</button>

                <input type="text" id="contact-search" placeholder="Search contacts..." class="search-input">
                <select id="contact-type-filter">
                    <option value="">All Types</option>
                    <option value="client">Clients</option>
                    <option value="supplier">Suppliers</option>
                    <option value="other">Other</option>
                </select>
                <select id="contact-sort" style="padding:8px;border-radius:4px;border:1px solid #d1d5db;">
                    <option value="name:ASC">Name (A-Z)</option>
                    <option value="name:DESC">Name (Z-A)</option>
                    <option value="last_contact:DESC" selected>Last Activity (Newest)</option>
                    <option value="last_contact:ASC">Last Activity (Oldest)</option>
                </select>
                <button class="btn btn-small" onclick="loadContacts()">Refresh</button>

                From: <input type="date" id="contact-date-from" style="padding:6px;border-radius:4px;border:1px solid #d1d5db;">
                To: <input type="date" id="contact-date-to" style="padding:6px;border-radius:4px;border:1px solid #d1d5db;">
                <button class="btn btn-sm" onclick="loadContacts(1)">Apply</button>
                <button class="btn btn-sm" style="background:#e5e7eb;color:#374151;" onclick="clearContactFilters()">Clear</button>
                <button class="btn btn-sm" style="background:#fef2f2;color:#ef4444;border:1px solid #ef4444;margin-left:auto;" onclick="promptBulkDeleteByEmail()">Bulk Delete by Email/Domain</button>

                    Import from Gmail:
                    <input type="text" id="sync-domain-input" placeholder="e.g. delta-engineering.be or ddb@delta-engineering.be" style="flex:1;padding:8px;border-radius:4px;border:1px solid #93c5fd;">
                    <button class="btn btn-primary" onclick="searchByDomain()">Search Gmail</button>

Loading contacts...

                <input type="text" id="email-search" placeholder="Search threads..." class="search-input">
                <select id="thread-sort" style="padding:8px;border-radius:4px;border:1px solid #d1d5db;">
                    <option value="last_date:DESC" selected>Last Activity (Newest)</option>
                    <option value="last_date:ASC">Last Activity (Oldest)</option>
                    <option value="email_count:DESC">Most Emails</option>
                    <option value="first_date:DESC">Newest Thread</option>
                </select>
                <button class="btn btn-small" onclick="loadEmails()">Refresh</button>

                <input type="text" id="thread-contact-filter" placeholder="Contact name..." style="padding:8px;border-radius:4px;border:1px solid #d1d5db;width:150px;">
                From: <input type="date" id="thread-date-from" style="padding:6px;border-radius:4px;border:1px solid #d1d5db;">
                To: <input type="date" id="thread-date-to" style="padding:6px;border-radius:4px;border:1px solid #d1d5db;">
                <select id="thread-status-filter" style="padding:8px;border-radius:4px;border:1px solid #d1d5db;">
                    <option value="">All Status</option>
                    <option value="analyzed">Analyzed</option>
                    <option value="not_analyzed">Not Analyzed</option>
                </select>
                <button class="btn btn-sm" onclick="loadEmails(1)">Apply</button>
                <button class="btn btn-sm" style="background:#e5e7eb;color:#374151;" onclick="clearThreadFilters()">Clear</button>
                <button class="btn btn-sm" id="delete-found-threads-btn" style="background:#ef4444;color:white;display:none;" onclick="deleteFoundThreads()">Delete All Found</button>

Click Refresh to load email threads

🛒 Product Search with AI

Search products from Amazon & AliExpress with advanced filters and AI recommendations

                        Search Query
                        <input type="text" id="quick-product-search" placeholder="e.g., LED strip lights COB waterproof" class="search-input" style="width: 100%;">

                        IP Rating
                        <select id="quick-filter-ip" class="search-input" style="width: 100%;">
                            <option value="">Any</option>
                            <option value="ip65">IP65</option>
                            <option value="ip67">IP67</option>
                            <option value="ip68">IP68</option>
                            <option value="ip6+">IP6+</option>
                        </select>

                        Length
                        <select id="quick-filter-length" class="search-input" style="width: 100%;">
                            <option value="">Any</option>
                            <option value="5m">5m</option>
                            <option value="10m">10m</option>
                            <option value="5m+">5m+</option>
                            <option value="10m+">10m+</option>
                        </select>

                        <button class="btn btn-primary" onclick="window.location.href='product_search.php'" style="background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%); border: none;">
                            Open Full Search
                        </button>

🎯 Smart Filters

                        Filter by IP rating (IP65, IP 65, ip6, etc.), length (5m, 5 m, 10 meters), and more.
                        Handles all format variations automatically.

🤖 AI Recommendations

                        Send your product list to AI and get best picks categorized as Best Value, Top Rated, and Recommended.

➕ Custom Filters

                        Add custom filters for COB type, wattage, voltage, or any text search. Extensible filter system.

                    🔍 Open Product Search Dashboard

📧 Gmail Account Management

Manage multiple Gmail accounts for email synchronization

Add New Gmail Account

                        Email Address
                        <input type="email" id="new-account-email" placeholder="account@gmail.com"
                               class="search-input" style="width: 100%;">

                        Display Name (Optional)
                        <input type="text" id="new-account-name" placeholder="Account Name"
                               class="search-input" style="width: 100%;">

                        <button class="btn btn-primary" onclick="addGmailAccount()" style="width: 100%;">Add Account</button>

Total Accounts

-

Active

-

Authenticated

-

Total Emails

-

                <button class="btn btn-primary" onclick="syncAllAccounts()">🔄 Sync All Accounts</button>
                <button class="btn btn-small" onclick="loadGmailAccounts()">Refresh</button>

Loading Gmail accounts...

🔬 Data Enrichment

Enrich contacts and companies with data from Apollo.io and web scraping

0

Pending

0

Processing

0

Completed

0

Failed

🔍 Enrich Single Contact

                    <input type="text" id="enrich-single-email" placeholder="Enter email address..." style="width: 100%; padding: 10px; border: 1px solid #d1d5db; border-radius: 6px; margin-bottom: 10px; box-sizing: border-box;">
                    <button class="btn btn-primary" onclick="enrichSingleContact()" style="width: 100%; background: #10b981; border: none; padding: 10px; border-radius: 6px; color: white; cursor: pointer;">
                        Enrich Contact
                    </button>

🏢 Enrich Single Company

                    <input type="text" id="enrich-single-domain" placeholder="Enter company domain..." style="width: 100%; padding: 10px; border: 1px solid #d1d5db; border-radius: 6px; margin-bottom: 10px; box-sizing: border-box;">
                    <button class="btn btn-primary" onclick="enrichSingleCompany()" style="width: 100%; background: #3b82f6; border: none; padding: 10px; border-radius: 6px; color: white; cursor: pointer;">
                        Enrich Company
                    </button>

📦 Batch Enrichment

                    <button class="btn" onclick="enrichAllContacts()" style="background: #10b981; color: white; border: none; padding: 12px; border-radius: 6px; cursor: pointer;">
                        👥 Enrich All Contacts
                    </button>
                    <button class="btn" onclick="enrichAllCompanies()" style="background: #3b82f6; color: white; border: none; padding: 12px; border-radius: 6px; cursor: pointer;">
                        🏢 Enrich All Companies
                    </button>
                    <button class="btn" onclick="processEnrichmentQueue()" style="background: #8b5cf6; color: white; border: none; padding: 12px; border-radius: 6px; cursor: pointer;">
                        ⚡ Process Queue
                    </button>
                    <button class="btn" onclick="loadEnrichmentStatus()" style="background: #6b7280; color: white; border: none; padding: 12px; border-radius: 6px; cursor: pointer;">
                        🔄 Refresh Status
                    </button>

📋 Recent Enrichment Activity

Click Refresh Status to load logs...

📊 Behavioral Pattern Detection

AI-powered detection of risky patterns across all email communications

Loading pattern statistics...

💼 Business Opportunities

AI-identified business opportunities, cross-sell suggestions, and deal insights from email analysis

Total Opportunities

-

Active Quotes

-

Cross-sell Ideas

-

Avg Deal Value

-

Loading business opportunities...

⚠ Risk Alerts

Automated alerts for high-risk behavioral patterns detected in communications

                <select id="alert-severity-filter" style="padding: 10px; border-radius: 6px; border: 1px solid #e0e0e0;">
                    <option value="">All Severities</option>
                    <option value="critical">Critical</option>
                    <option value="high">High</option>
                    <option value="medium">Medium</option>
                    <option value="low">Low</option>
                </select>
                <select id="alert-status-filter" style="padding: 10px; border-radius: 6px; border: 1px solid #e0e0e0;">
                    <option value="unread">Unread</option>
                    <option value="all">All</option>
                    <option value="read">Read</option>
                </select>
                <button class="btn btn-small" onclick="loadAlerts()">Refresh</button>

Loading alerts...

🔍 Semantic Search

Search across all emails using natural language. Find patterns, behaviors, and similar situations.

                    <input type="text" id="semantic-search-input" placeholder="e.g., 'payment delays', 'lies about deadlines', 'scope creep'..."
                           style="flex: 1; min-width: 300px; padding: 12px; border-radius: 6px; border: none; font-size: 1em;">
                    <select id="search-type" style="padding: 12px; border-radius: 6px; border: none;">
                        <option value="">All Types</option>
                        <option value="email">Emails</option>
                        <option value="thread_summary">Thread Summaries</option>
                        <option value="pattern">Detected Patterns</option>
                    </select>
                    <button class="btn" onclick="semanticSearch()" style="background: white; color: #667eea; padding: 12px 24px;">Search</button>

                Example searches:

                    <button class="btn btn-small" onclick="quickSearch('payment delay excuses')">Payment delays</button>
                    <button class="btn btn-small" onclick="quickSearch('changed story inconsistency')">Story changes</button>
                    <button class="btn btn-small" onclick="quickSearch('price negotiation aggressive')">Price negotiation</button>
                    <button class="btn btn-small" onclick="quickSearch('blame shifting responsibility')">Blame shifting</button>
                    <button class="btn btn-small" onclick="quickSearch('scope creep additional work')">Scope creep</button>
                    <button class="btn btn-small" onclick="quickSearch('competitor mention shopping around')">Competitor mentions</button>

                Index Management:

                    <button class="btn btn-small" onclick="indexVectors('all')">Index All Emails</button>
                    <button class="btn btn-small" onclick="checkVectorStatus()">Check Status</button>

Enter a search query above to find relevant emails and patterns.

                <select id="quote-outcome-filter">
                    <option value="">All Outcomes</option>
                    <option value="pending">Pending</option>
                    <option value="accepted">Accepted</option>
                    <option value="rejected">Rejected</option>
                    <option value="negotiated">Negotiated</option>
                </select>
                <button class="btn btn-small" onclick="loadQuotes()">Refresh</button>

Loading quotes...

                <input type="text" id="new-filter" placeholder="Email or @domain.com" class="search-input">
                <input type="text" id="filter-description" placeholder="Description (optional)" class="search-input">
                <button class="btn btn-primary" onclick="addFilter()">Add Filter</button>

                Add email addresses or domains (starting with @) to filter which emails are synced.
                Only emails to/from these addresses will be downloaded.

Loading filters...

💡 Business Ideas Research

AI-powered research into business verticals, horizontals, and growth scenarios using web scraping and analysis

📊 Your Business Context

                    Click to set/update your financial goals (helps AI provide better recommendations)

                            Revenue Target
                            <input type="text" id="owner-revenue-target" placeholder="e.g., $500K/year, 2x current revenue" style="width: 100%; padding: 10px; border: 1px solid #d1d5db; border-radius: 6px; box-sizing: border-box;">

                            Investment Capacity
                            <input type="text" id="owner-investment-capacity" placeholder="e.g., $50K available for new ventures" style="width: 100%; padding: 10px; border: 1px solid #d1d5db; border-radius: 6px; box-sizing: border-box;">

                            Growth Objectives
                            <input type="text" id="owner-growth-objectives" placeholder="e.g., Expand to new markets, diversify products" style="width: 100%; padding: 10px; border: 1px solid #d1d5db; border-radius: 6px; box-sizing: border-box;">

                            Timeline
                            <input type="text" id="owner-timeline" placeholder="e.g., 12-18 months" style="width: 100%; padding: 10px; border: 1px solid #d1d5db; border-radius: 6px; box-sizing: border-box;">

                            Additional Notes
                            <textarea id="owner-notes" placeholder="Any other relevant information about your business situation..." style="width: 100%; padding: 10px; border: 1px solid #d1d5db; border-radius: 6px; min-height: 60px; box-sizing: border-box;"></textarea>

                            <button class="btn btn-primary" onclick="saveOwnerStatus()" style="background: #8b5cf6;">Save Business Context</button>

Start New Research Project

                    Research Query
                    <textarea id="business-ideas-query"
                        placeholder="e.g., Research 50 business verticals related to LED lighting and smart home technology. For each vertical, identify 20-30 horizontal market segments. Focus on B2B opportunities, emerging markets, and areas with high growth potential."
                        style="width: 100%; min-height: 100px; padding: 12px; border: 1px solid #d1d5db; border-radius: 6px; resize: vertical; box-sizing: border-box;"></textarea>

                    Advanced Options

                                Target Verticals
                                <input type="number" id="param-verticals" value="50" min="1" max="100" style="width: 100%; padding: 8px; border: 1px solid #d1d5db; border-radius: 4px; box-sizing: border-box;">

                                Horizontals per Vertical
                                <input type="number" id="param-horizontals" value="25" min="1" max="50" style="width: 100%; padding: 8px; border: 1px solid #d1d5db; border-radius: 4px; box-sizing: border-box;">

                                Research Depth
                                <select id="param-depth" style="width: 100%; padding: 8px; border: 1px solid #d1d5db; border-radius: 4px; box-sizing: border-box;">
                                    <option value="shallow">Shallow (Fast)</option>
                                    <option value="medium" selected>Medium</option>
                                    <option value="deep">Deep (Thorough)</option>
                                </select>

                                Scenario Depth
                                <select id="param-scenario-depth" style="width: 100%; padding: 8px; border: 1px solid #d1d5db; border-radius: 4px; box-sizing: border-box;">
                                    <option value="high_level" selected>High-Level Strategic</option>
                                    <option value="detailed">Detailed Action Plans</option>
                                    <option value="financial">Financial Projections</option>
                                </select>

                <button class="btn btn-primary" onclick="createBusinessIdeasProject()" style="background: linear-gradient(135deg, #8b5cf6 0%, #6366f1 100%); padding: 12px 24px;">
                    🚀 Start Research Project
                </button>

0

Pending

0

Running

0

Completed

0

Failed

0

Findings

                <button class="btn btn-primary" onclick="processBusinessIdeasQueue()" style="background: #8b5cf6;">
                    ⚡ Process Queue
                </button>
                <button class="btn btn-small" onclick="loadBusinessIdeasProjects()">🔄 Refresh</button>

Loading research projects...

                ×

                ×

                ×

                    AI

AI Assistant

General Chat

                    <button onclick="startNewSession()" title="New chat" style="background:rgba(255,255,255,0.15); border:none; color:#fff; border-radius:6px; padding:4px 8px; cursor:pointer; font-size:0.7em;">+ New</button>
                    <button onclick="loadChatSessions()" title="Chat history" style="background:rgba(255,255,255,0.15); border:none; color:#fff; border-radius:6px; padding:4px 8px; cursor:pointer; font-size:0.7em;">History</button>
                    <button onclick="clearAiChat()" title="Clear chat" style="background:rgba(255,255,255,0.15); border:none; color:#fff; border-radius:6px; padding:4px 8px; cursor:pointer; font-size:0.7em;">Clear</button>
                    <button onclick="toggleAiChat()" style="background:rgba(255,255,255,0.15); border:none; color:#fff; border-radius:6px; padding:4px 8px; cursor:pointer; font-size:1em;">X</button>

                    <select id="ai-chat-provider" onchange="updateAiModelDropdown()" style="width:100px; background:#1e1e2e; color:#e2e8f0; border:1px solid #3d3d5c; border-radius:6px; padding:3px 6px; font-size:0.72em; cursor:pointer;">

                    </select>
                    <select id="ai-chat-model" style="flex:1; background:#1e1e2e; color:#e2e8f0; border:1px solid #3d3d5c; border-radius:6px; padding:3px 6px; font-size:0.72em; cursor:pointer;">

                    </select>

                    <button class="ai-agent-btn" data-agent="general" onclick="selectAgent('general')" style="background:#6366f1; color:#fff; border:none; border-radius:5px; padding:3px 8px; font-size:0.68em; cursor:pointer;">General</button>
                    <button class="ai-agent-btn" data-agent="quote_analyst" onclick="selectAgent('quote_analyst')" style="background:#2d2d4e; color:#94a3b8; border:none; border-radius:5px; padding:3px 8px; font-size:0.68em; cursor:pointer;">Quotes</button>
                    <button class="ai-agent-btn" data-agent="deception_detector" onclick="selectAgent('deception_detector')" style="background:#2d2d4e; color:#94a3b8; border:none; border-radius:5px; padding:3px 8px; font-size:0.68em; cursor:pointer;">Deception</button>
                    <button class="ai-agent-btn" data-agent="contact_profiler" onclick="selectAgent('contact_profiler')" style="background:#2d2d4e; color:#94a3b8; border:none; border-radius:5px; padding:3px 8px; font-size:0.68em; cursor:pointer;">Profiler</button>
                    <button class="ai-agent-btn" data-agent="email_composer" onclick="selectAgent('email_composer')" style="background:#2d2d4e; color:#94a3b8; border:none; border-radius:5px; padding:3px 8px; font-size:0.68em; cursor:pointer;">Composer</button>

                    Search contacts/threads to add context...
                    +

                        <input id="ai-context-search-input" type="text" placeholder="Search contacts or threads by name, email, subject..."
                               style="flex:1; background:#252540; color:#e2e8f0; border:1px solid #3d3d5c; border-radius:6px; padding:5px 8px; font-size:0.72em; outline:none;"
                               onkeydown="if(event.key==='Enter')searchContextContacts()">
                        <button onclick="searchContextContacts()" style="background:#6366f1; color:#fff; border:none; border-radius:6px; padding:4px 10px; font-size:0.7em; cursor:pointer;">Search</button>

AI

Search a contact, add threads, select an agent, and ask away.

Or open a thread/contact first for quick context.

                    <textarea id="ai-chat-input" placeholder="Ask anything... (Enter to send, Shift+Enter for newline)" style="flex:1; background:#1e1e2e; color:#e2e8f0; border:1px solid #3d3d5c; border-radius:8px; padding:8px 10px; font-size:0.82em; resize:none; height:50px; font-family:inherit; outline:none;" onkeydown="handleAiChatKey(event)"></textarea>
                    <button onclick="sendAiChat()" id="ai-chat-send-btn" style="background:linear-gradient(135deg,#6366f1,#8b5cf6); color:#fff; border:none; border-radius:8px; padding:8px 14px; cursor:pointer; font-size:1.1em; height:50px; min-width:44px; transition:opacity 0.2s;">Send</button>

        <button id="ai-chat-fab" onclick="toggleAiChat()" title="AI Assistant" style="position:fixed; bottom:20px; right:20px; width:56px; height:56px; background:linear-gradient(135deg,#6366f1,#8b5cf6); color:#fff; border:none; border-radius:50%; font-size:1.5em; cursor:pointer; box-shadow:0 4px 20px rgba(99,102,241,0.5); z-index:9998; transition:transform 0.2s; display:flex; align-items:center; justify-content:center;" onmouseover="this.style.transform='scale(1.1)'" onmouseout="this.style.transform='scale(1)'">AI</button>

Processing...

-                    Last: <?= $syncStatus['last_sync_at'] ? date('M j, H:i', strtotime($syncStatus['last_sync_at'])) : 'Never' ?>

+                    Last: <?= !empty($syncStatus['last_sync_at']) ? date('M j, H:i', strtotime($syncStatus['last_sync_at'])) : 'Never' ?>


TodoWrite

Fix GmailAccount::getStats() - null-coalesce missing fields

Fixing GmailAccount::getStats() undefined index error

Update search_gmail_threads to search all connected accounts

Updating search_gmail_threads to search all accounts

Update import_gmail_threads to use correct account

Updating import_gmail_threads for multi-account

Verify with php -l syntax check


Verify with php -l syntax check

Verify with php -l syntax check


**Fix 2:** Update `search_gmail_threads` to search all connected accounts, and `import_gmail_threads` to use the right account.


  5745→        // ==========================================
  5746→        // Search Gmail Threads by email/domain
  5747→        // ==========================================
  5748→        case 'search_gmail_threads':
  5749→            $query = trim($_GET['query'] ?? '');
  5750→            if (!$query) jsonError('query required');
  5751→            $maxResults = min(50, (int)($_GET['max_results'] ?? 20));
  5752→
  5753→            try {
  5754→                // Build Gmail search query
  5755→                if (strpos($query, '@') !== false) {
  5756→                    $gmailQuery = "from:{$query} OR to:{$query}";
  5757→                } else {
  5758→                    $gmailQuery = "from:@{$query} OR to:@{$query}";
  5759→                }
  5760→
  5761→                $gmail = new \EmailIntelligence\GmailClient();
  5762→                $threads = $gmail->searchThreads($gmailQuery, $maxResults);
  5763→
  5764→                // Check which threads are already imported
  5765→                $syncMongo = \EmailIntelligence\MongoDatabase::getInstance();
  5766→                foreach ($threads as &$t) {
  5767→                    $existing = $syncMongo->count('emails', ['thread_id' => $t['id']]);
  5768→                    $t['already_imported'] = $existing > 0;
  5769→                    $t['email_count'] = $existing;
  5770→                }
  5771→                unset($t);
  5772→
  5773→                jsonResponse(['success' => true, 'threads' => $threads, 'query' => $gmailQuery, 'total' => count($threads)]);
  5774→            } catch (Exception $e) {
  5775→                jsonError('Gmail search failed: ' . $e->getMessage());
  5776→            }
  5777→            break;
  5778→
  5779→        // ==========================================
  5780→        // Import Gmail Threads - bulk import selected threads
  5781→        // ==========================================
  5782→        case 'import_gmail_threads':
  5783→            if ($_SERVER['REQUEST_METHOD'] !== 'POST') jsonError('POST required', 405);
  5784→            $jsonData = json_decode(file_get_contents('php://input'), true) ?? [];
  5785→            $threadIds = $jsonData['thread_ids'] ?? [];
  5786→            if (empty($threadIds)) jsonError('thread_ids required');
  5787→
  5788→            try {
  5789→                $gmail = new \EmailIntelligence\GmailClient();
  5790→                $userEmail = strtolower(GMAIL_USER_EMAIL);
  5791→                $results = [];
  5792→
  5793→                foreach ($threadIds as $tid) {
  5794→                    try {
  5795→                        $threadData = $gmail->getThread($tid);
  5796→                        if (empty($threadData['messages'])) {
  5797→                            $results[] = ['thread_id' => $tid, 'success' => false, 'error' => 'No messages'];
  5798→                            continue;
  5799→                        }
  5800→
  5801→                        $saved = 0;
  5802→                        $contactId = null;
  5803→                        $contactsCreated = [];
  5804→
  5805→                        foreach ($threadData['messages'] as $emailData) {
  5806→                            try {
  5807→                                $fromEmail = strtolower($emailData['from_email'] ?? '');
  5808→                                $fromName = $emailData['from_name'] ?? '';
  5809→
  5810→                                $emailContactId = null;
  5811→                                if ($fromEmail && $fromEmail !== $userEmail) {
  5812→                                    if (isset($contactsCreated[$fromEmail])) {
  5813→                                        $emailContactId = $contactsCreated[$fromEmail];
  5814→                                    } else {
  5815→                                        $existingContact = $db->fetchOne(
  5816→                                            "SELECT id FROM contacts WHERE LOWER(email) = ?",
  5817→                                            [$fromEmail]
  5818→                                        );
  5819→                                        if ($existingContact) {
  5820→                                            $emailContactId = $existingContact['id'];
  5821→                                        } else {
  5822→                                            $db->insert('contacts', [
  5823→                                                'name' => $fromName ?: ucfirst(explode('@', $fromEmail)[0]),
  5824→                                                'email' => $fromEmail,
  5825→                                                'company' => '',
  5826→                                                'created_at' => date('Y-m-d H:i:s')
  5827→                                            ]);
  5828→                                            $emailContactId = $db->lastInsertId();
  5829→                                        }
  5830→                                        $contactsCreated[$fromEmail] = $emailContactId;
  5831→                                    }
  5832→                                    if (!$contactId) $contactId = $emailContactId;
  5833→                                }
  5834→

        // ==========================================
        // Search Gmail Threads by email/domain
        // ==========================================
        case 'search_gmail_threads':
            $query = trim($_GET['query'] ?? '');
            if (!$query) jsonError('query required');
            $maxResults = min(50, (int)($_GET['max_results'] ?? 20));

            try {
                // Build Gmail search query
                if (strpos($query, '@') !== false) {
                    $gmailQuery = "from:{$query} OR to:{$query}";
                } else {
                    $gmailQuery = "from:@{$query} OR to:@{$query}";
                }

                $gmail = new \EmailIntelligence\GmailClient();
                $threads = $gmail->searchThreads($gmailQuery, $maxResults);

                // Check which threads are already imported
                $syncMongo = \EmailIntelligence\MongoDatabase::getInstance();
                foreach ($threads as &$t) {
                    $existing = $syncMongo->count('emails', ['thread_id' => $t['id']]);
                    $t['already_imported'] = $existing > 0;
                    $t['email_count'] = $existing;
                }
                unset($t);

                jsonResponse(['success' => true, 'threads' => $threads, 'query' => $gmailQuery, 'total' => count($threads)]);
            } catch (Exception $e) {
                jsonError('Gmail search failed: ' . $e->getMessage());
            }
            break;

        // ==========================================
        // Import Gmail Threads - bulk import selected threads
        // ==========================================
        case 'import_gmail_threads':
            if ($_SERVER['REQUEST_METHOD'] !== 'POST') jsonError('POST required', 405);
            $jsonData = json_decode(file_get_contents('php://input'), true) ?? [];
            $threadIds = $jsonData['thread_ids'] ?? [];
            if (empty($threadIds)) jsonError('thread_ids required');

            try {
                $gmail = new \EmailIntelligence\GmailClient();
                $userEmail = strtolower(GMAIL_USER_EMAIL);
                $results = [];

                foreach ($threadIds as $tid) {
                    try {
                        $threadData = $gmail->getThread($tid);
                        if (empty($threadData['messages'])) {
                            $results[] = ['thread_id' => $tid, 'success' => false, 'error' => 'No messages'];
                            continue;
                        }

                        $saved = 0;
                        $contactId = null;
                        $contactsCreated = [];

                        foreach ($threadData['messages'] as $emailData) {
                            try {
                                $fromEmail = strtolower($emailData['from_email'] ?? '');
                                $fromName = $emailData['from_name'] ?? '';

                                $emailContactId = null;
                                if ($fromEmail && $fromEmail !== $userEmail) {
                                    if (isset($contactsCreated[$fromEmail])) {
                                        $emailContactId = $contactsCreated[$fromEmail];
                                    } else {
                                        $existingContact = $db->fetchOne(
                                            "SELECT id FROM contacts WHERE LOWER(email) = ?",
                                            [$fromEmail]
                                        );
                                        if ($existingContact) {
                                            $emailContactId = $existingContact['id'];
                                        } else {
                                            $db->insert('contacts', [
                                                'name' => $fromName ?: ucfirst(explode('@', $fromEmail)[0]),
                                                'email' => $fromEmail,
                                                'company' => '',
                                                'created_at' => date('Y-m-d H:i:s')
                                            ]);
                                            $emailContactId = $db->lastInsertId();
                                        }
                                        $contactsCreated[$fromEmail] = $emailContactId;
                                    }
                                    if (!$contactId) $contactId = $emailContactId;
                                }


  5834→
  5835→                                // Save email
  5836→                                $db->insertOrUpdate('emails', [
  5837→                                    'gmail_id' => $emailData['gmail_id'],
  5838→                                    'thread_id' => $tid,
  5839→                                    'contact_id' => $emailContactId ?: $contactId,
  5840→                                    'subject' => $emailData['subject'] ?? '',
  5841→                                    'from_email' => $emailData['from_email'] ?? '',
  5842→                                    'from_name' => $fromName,
  5843→                                    'to_email' => $emailData['to_email'] ?? '',
  5844→                                    'to_name' => $emailData['to_name'] ?? '',
  5845→                                    'cc_email' => $emailData['cc_email'] ?? '',
  5846→                                    'cc_name' => $emailData['cc_name'] ?? '',
  5847→                                    'body_text' => $emailData['body_text'] ?? '',
  5848→                                    'body_html' => $emailData['body_html'] ?? '',
  5849→                                    'date' => $emailData['date'] ?? date('Y-m-d H:i:s'),
  5850→                                    'direction' => $emailData['direction'] ?? 'inbound',
  5851→                                    'has_attachments' => !empty($emailData['has_attachments']) ? 1 : 0
  5852→                                ], ['gmail_id']);
  5853→                                $saved++;
  5854→                            } catch (Exception $e) {
  5855→                                error_log("import_gmail_threads: Failed email: " . $e->getMessage());
  5856→                            }
  5857→                        }
  5858→
  5859→                        // Create thread_analysis record
  5860→                        $db->insertOrUpdate('thread_analysis', [
  5861→                            'thread_id' => $tid,
  5862→                            'contact_id' => $contactId,
  5863→                            'email_count' => $saved,
  5864→                            'last_email_date' => $threadData['messages'][count($threadData['messages']) - 1]['date'] ?? date('Y-m-d H:i:s'),
  5865→                            'is_analyzed' => 0,
  5866→                            'needs_reanalysis' => 1
  5867→                        ], ['thread_id']);
  5868→
  5869→                        $results[] = ['thread_id' => $tid, 'success' => true, 'emails_saved' => $saved];
  5870→                    } catch (Exception $e) {
  5871→                        $results[] = ['thread_id' => $tid, 'success' => false, 'error' => $e->getMessage()];
  5872→                    }
  5873→                }
  5874→
  5875→                $successCount = count(array_filter($results, function($r) { return $r['success']; }));
  5876→                jsonResponse(['success' => true, 'results' => $results, 'imported' => $successCount, 'total' => count($threadIds)]);
  5877→            } catch (Exception $e) {
  5878→                jsonError('Import failed: ' . $e->getMessage());
  5879→            }
  5880→            break;
  5881→
  5882→        // ==========================================
  5883→        // Delete Contact - cascade delete all associated data
  5884→        // ==========================================
  5885→        case 'delete_contact':
  5886→            if ($_SERVER['REQUEST_METHOD'] !== 'POST') jsonError('POST required', 405);
  5887→            $jsonData = json_decode(file_get_contents('php://input'), true) ?? [];
  5888→            $contactId = (int)($jsonData['contact_id'] ?? 0);
  5889→            if (!$contactId) jsonError('contact_id required');
  5890→            $adapter = QueryAdapter::getInstance();
  5891→            $deleted = $adapter->deleteContact($contactId);
  5892→            jsonResponse(['success' => true, 'deleted' => $deleted]);
  5893→            break;

                                // Save email
                                $db->insertOrUpdate('emails', [
                                    'gmail_id' => $emailData['gmail_id'],
                                    'thread_id' => $tid,
                                    'contact_id' => $emailContactId ?: $contactId,
                                    'subject' => $emailData['subject'] ?? '',
                                    'from_email' => $emailData['from_email'] ?? '',
                                    'from_name' => $fromName,
                                    'to_email' => $emailData['to_email'] ?? '',
                                    'to_name' => $emailData['to_name'] ?? '',
                                    'cc_email' => $emailData['cc_email'] ?? '',
                                    'cc_name' => $emailData['cc_name'] ?? '',
                                    'body_text' => $emailData['body_text'] ?? '',
                                    'body_html' => $emailData['body_html'] ?? '',
                                    'date' => $emailData['date'] ?? date('Y-m-d H:i:s'),
                                    'direction' => $emailData['direction'] ?? 'inbound',
                                    'has_attachments' => !empty($emailData['has_attachments']) ? 1 : 0
                                ], ['gmail_id']);
                                $saved++;
                            } catch (Exception $e) {
                                error_log("import_gmail_threads: Failed email: " . $e->getMessage());
                            }
                        }

                        // Create thread_analysis record
                        $db->insertOrUpdate('thread_analysis', [
                            'thread_id' => $tid,
                            'contact_id' => $contactId,
                            'email_count' => $saved,
                            'last_email_date' => $threadData['messages'][count($threadData['messages']) - 1]['date'] ?? date('Y-m-d H:i:s'),
                            'is_analyzed' => 0,
                            'needs_reanalysis' => 1
                        ], ['thread_id']);

                        $results[] = ['thread_id' => $tid, 'success' => true, 'emails_saved' => $saved];
                    } catch (Exception $e) {
                        $results[] = ['thread_id' => $tid, 'success' => false, 'error' => $e->getMessage()];
                    }
                }

                $successCount = count(array_filter($results, function($r) { return $r['success']; }));
                jsonResponse(['success' => true, 'results' => $results, 'imported' => $successCount, 'total' => count($threadIds)]);
            } catch (Exception $e) {
                jsonError('Import failed: ' . $e->getMessage());
            }
            break;

        // ==========================================
        // Delete Contact - cascade delete all associated data
        // ==========================================
        case 'delete_contact':
            if ($_SERVER['REQUEST_METHOD'] !== 'POST') jsonError('POST required', 405);
            $jsonData = json_decode(file_get_contents('php://input'), true) ?? [];
            $contactId = (int)($jsonData['contact_id'] ?? 0);
            if (!$contactId) jsonError('contact_id required');
            $adapter = QueryAdapter::getInstance();
            $deleted = $adapter->deleteContact($contactId);
            jsonResponse(['success' => true, 'deleted' => $deleted]);
            break;


Now I need to check how `GmailClient::searchThreads()` works to understand if I can call it for each account.


577:    public function searchThreads(string $query, int $maxResults = 10): array


   577→    public function searchThreads(string $query, int $maxResults = 10): array
   578→    {
   579→        try {
   580→            $service = $this->getService();
   581→
   582→            $params = [
   583→                'q' => $query,
   584→                'maxResults' => $maxResults
   585→            ];
   586→
   587→            $response = $service->users_threads->listUsersThreads('me', $params);
   588→            $threads = [];
   589→
   590→            foreach ($response->getThreads() ?? [] as $thread) {
   591→                $threads[] = [
   592→                    'id' => $thread->getId(),
   593→                    'snippet' => $thread->getSnippet()
   594→                ];
   595→            }
   596→
   597→            return $threads;
   598→
   599→        } catch (Exception $e) {
   600→            $this->logError("searchThreads failed: " . $e->getMessage());
   601→            throw $e;
   602→        }
   603→    }
   604→
   605→    /**
   606→     * Parse Gmail message into structured array
   607→     *
   608→     * @param Message $message Gmail message object
   609→     * @return array Structured message data
   610→     */
   611→    private function parseMessage(Message $message): array
   612→    {
   613→        $payload = $message->getPayload();
   614→        $headers = $this->parseHeaders($payload->getHeaders());
   615→
   616→        // Extract body
   617→        $bodyText = '';
   618→        $bodyHtml = '';
   619→        $this->extractBody($payload, $bodyText, $bodyHtml);
   620→
   621→        // Parse email addresses
   622→        $from = $this->parseEmailAddress($headers['From'] ?? '');
   623→        $to = $this->parseEmailAddress($headers['To'] ?? '');
   624→        $cc = $this->parseEmailAddress($headers['Cc'] ?? '');
   625→
   626→        // Determine direction

    public function searchThreads(string $query, int $maxResults = 10): array
    {
        try {
            $service = $this->getService();

            $params = [
                'q' => $query,
                'maxResults' => $maxResults
            ];

            $response = $service->users_threads->listUsersThreads('me', $params);
            $threads = [];

            foreach ($response->getThreads() ?? [] as $thread) {
                $threads[] = [
                    'id' => $thread->getId(),
                    'snippet' => $thread->getSnippet()
                ];
            }

            return $threads;

        } catch (Exception $e) {
            $this->logError("searchThreads failed: " . $e->getMessage());
            throw $e;
        }
    }

    /**
     * Parse Gmail message into structured array
     *
     * @param Message $message Gmail message object
     * @return array Structured message data
     */
    private function parseMessage(Message $message): array
    {
        $payload = $message->getPayload();
        $headers = $this->parseHeaders($payload->getHeaders());

        // Extract body
        $bodyText = '';
        $bodyHtml = '';
        $this->extractBody($payload, $bodyText, $bodyHtml);

        // Parse email addresses
        $from = $this->parseEmailAddress($headers['From'] ?? '');
        $to = $this->parseEmailAddress($headers['To'] ?? '');
        $cc = $this->parseEmailAddress($headers['Cc'] ?? '');

        // Determine direction


Now I'll update `search_gmail_threads` to loop through all authenticated accounts and merge results, and update `import_gmail_threads` to accept an optional `account_id` parameter.


Edit

        case 'search_gmail_threads':
            $query = trim($_GET['query'] ?? '');
            if (!$query) jsonError('query required');
            $maxResults = min(50, (int)($_GET['max_results'] ?? 20));

            try {
                // Build Gmail search query
                if (strpos($query, '@') !== false) {
                    $gmailQuery = "from:{$query} OR to:{$query}";
                } else {
                    $gmailQuery = "from:@{$query} OR to:@{$query}";
                }

                $gmail = new \EmailIntelligence\GmailClient();
                $threads = $gmail->searchThreads($gmailQuery, $maxResults);

                // Check which threads are already imported
                $syncMongo = \EmailIntelligence\MongoDatabase::getInstance();
                foreach ($threads as &$t) {
                    $existing = $syncMongo->count('emails', ['thread_id' => $t['id']]);
                    $t['already_imported'] = $existing > 0;
                    $t['email_count'] = $existing;
                }
                unset($t);

                jsonResponse(['success' => true, 'threads' => $threads, 'query' => $gmailQuery, 'total' => count($threads)]);
            } catch (Exception $e) {
                jsonError('Gmail search failed: ' . $e->getMessage());
            }
            break;

        case 'search_gmail_threads':
            $query = trim($_GET['query'] ?? '');
            if (!$query) jsonError('query required');
            $maxResults = min(50, (int)($_GET['max_results'] ?? 20));

            try {
                // Build Gmail search query
                if (strpos($query, '@') !== false) {
                    $gmailQuery = "from:{$query} OR to:{$query}";
                } else {
                    $gmailQuery = "from:@{$query} OR to:@{$query}";
                }

                // Search across ALL authenticated Gmail accounts
                $accountManager = new \EmailIntelligence\GmailAccount();
                $allAccounts = $accountManager->getAccountsForSync();
                $allThreads = [];
                $seenThreadIds = [];
                $accountsSearched = [];

                foreach ($allAccounts as $acct) {
                    try {
                        $gmail = new \EmailIntelligence\GmailClient((int)$acct['id']);
                        $threads = $gmail->searchThreads($gmailQuery, $maxResults);
                        foreach ($threads as $t) {
                            if (!isset($seenThreadIds[$t['id']])) {
                                $t['account_id'] = (int)$acct['id'];
                                $t['account_email'] = $acct['email'];
                                $allThreads[] = $t;
                                $seenThreadIds[$t['id']] = true;
                            }
                        }
                        $accountsSearched[] = $acct['email'];
                    } catch (Exception $e) {
                        // Skip accounts that fail (e.g. expired token)
                        error_log("search_gmail_threads: Account {$acct['email']} failed: " . $e->getMessage());
                    }
                }

                // Fallback: if no accounts found, try default
                if (empty($allAccounts)) {
                    $gmail = new \EmailIntelligence\GmailClient();
                    $allThreads = $gmail->searchThreads($gmailQuery, $maxResults);
                    $accountsSearched[] = GMAIL_USER_EMAIL;
                }

                // Check which threads are already imported
                $syncMongo = \EmailIntelligence\MongoDatabase::getInstance();
                foreach ($allThreads as &$t) {
                    $existing = $syncMongo->count('emails', ['thread_id' => $t['id']]);
                    $t['already_imported'] = $existing > 0;
                    $t['email_count'] = $existing;
                }
                unset($t);

                jsonResponse([
                    'success' => true,
                    'threads' => $allThreads,
                    'query' => $gmailQuery,
                    'total' => count($allThreads),
                    'accounts_searched' => $accountsSearched
                ]);
            } catch (Exception $e) {
                jsonError('Gmail search failed: ' . $e->getMessage());
            }
            break;