UNPKG

besper-frontend-site-dev-main

Version:

Professional B-esper Frontend Site - Site-wide integration toolkit for full website bot deployment

832 lines (743 loc) 27.7 kB
/** * My Bots Page Script - Comprehensive bot management with APIM integration * Migrated from besper_site_serversite with full business logic preservation */ class MyBotsPage { constructor(options = {}) { this.options = { containerId: 'besper-site-content', environment: 'prod', ...options, }; this.initialized = false; this.currentBotId = null; this.bots = []; this.costPools = []; this.currentLanguage = 'en'; // BULLETPROOF: Defer all complex operations to avoid blocking class export this.authService = null; this.operatorsService = null; this.isAuthenticated = false; // Initialize auth safely in next tick to ensure class export completes first if (typeof window !== 'undefined') { setTimeout(() => { this.initializeAuth(); }, 1); } } /** * Initialize authentication safely without blocking class export */ initializeAuth() { try { this.authService = this.getAuthService(); this.operatorsService = this.getOperatorsService(); // Set up internationalization this.setupInternationalization(); } catch (error) { console.warn('Auth initialization failed, using fallback:', error); this.authService = this.createFallbackAuthService(); this.operatorsService = this.createFallbackOperatorsService(); } } /** * Set up internationalization from browser settings */ setupInternationalization() { const browserLang = navigator.language.slice(0, 2); this.currentLanguage = ['en', 'de'].includes(browserLang) ? browserLang : 'en'; console.log( `[BotManagement] [API] Language set to: ${this.currentLanguage}` ); } /** * Get authentication service from global scope or create simple fallback */ getAuthService() { // Try to access global token auth service if available if (typeof window !== 'undefined' && window.tokenAuthService) { return window.tokenAuthService; } return this.createFallbackAuthService(); } /** * Get operators service from global scope or create simple fallback */ getOperatorsService() { // Try to access global page operators service if available if (typeof window !== 'undefined' && window.pageOperatorsService) { return window.pageOperatorsService; } return this.createFallbackOperatorsService(); } /** * Create safe fallback auth service with comprehensive token management */ createFallbackAuthService() { return { isUserAuthenticated: () => { try { return ( typeof window !== 'undefined' && window.auth && typeof window.auth.getToken === 'function' && !!window.auth.getToken() ); } catch (error) { return false; } }, getToken: () => { try { if ( typeof window !== 'undefined' && window.auth && typeof window.auth.getToken === 'function' ) { return window.auth.getToken(); } } catch (error) { console.warn('Token access failed:', error); } return null; }, getUserPermission: key => { try { if (typeof window === 'undefined') return null; const mappings = { contactId: window.contact_id || window.user_contactid, userName: window.user_name, name: window.user_name, userEmail: window.user_email, email: window.user_email, workspaceId: window.workspace_id, accountId: window.account_id, subscriptionId: window.subscription_id, }; return mappings[key] || null; } catch (error) { console.warn('User permission access failed:', error); return null; } }, }; } /** * Create safe fallback operators service */ createFallbackOperatorsService() { return { callOperator: async () => { console.warn( 'Operators service not available, returning empty response' ); return { success: false, message: 'Service not available' }; }, }; } /** * Initialize the page with comprehensive bot management features */ async initialize(_data = {}) { if (this.initialized) return; try { console.log( '[BotManagement] [INIT] Initializing bot management interface...' ); // Load initial data with APIM integration await this.loadInitialData(); // Setup event handlers this.setupEventHandlers(); // Check URL parameters this.handleUrlParameters(); // Render the comprehensive interface this.renderInterface(); this.initialized = true; console.log( '[BotManagement] [SUCCESS] Bot management interface initialized successfully' ); } catch (error) { console.error('Error initializing my bots page:', error); this.showError(error); } } /** * Load initial data with APIM integration and user impersonation */ async loadInitialData() { try { console.log('[BotManagement] 📊 Loading bot data from APIM...'); const token = await this.getAuthToken(); if (!token) { console.warn( '[BotManagement] [WARN] No authentication token available' ); this.showAuthenticationError(); return; } await this.loadBots(token); await this.loadCostPools(token); } catch (error) { console.error( '[BotManagement] [ERROR] Error loading initial data:', error ); this.showErrorMessage( 'Failed to load bot data. Please refresh the page.' ); } } /** * Get authentication token using PROFESSIONAL token coordination system */ async getAuthToken() { try { // Use PROFESSIONAL cookie-based token manager (infinite-loop-proof) if (window.professionalTokenManager) { const token = await window.professionalTokenManager.getToken('BotManagement'); if (token) { console.log( '[BotManagement] [SECURITY] Got token from PROFESSIONAL manager' ); return token; } } // Fallback to cookie-based token manager if professional not available if (window.cookieTokenManager) { const token = await window.cookieTokenManager.getToken('BotManagement'); if (token) { console.log('[BotManagement] 🍪 Got token from cookie manager'); return token; } } // Fallback to global functions if (window.generateToken) { const token = await window.generateToken('BotManagement'); if (token) { console.log('[BotManagement] 🔑 Got token from global generateToken'); return token; } } // Final fallback to auth service const fallbackToken = this.authService?.getToken(); if (fallbackToken) { console.log('[BotManagement] 🔑 Got token from auth service fallback'); return fallbackToken; } console.warn('[BotManagement] [WARN] No token available from any source'); return null; } catch (error) { console.error('[BotManagement] [ERROR] Token generation failed:', error); return null; } } /** * Load bots from APIM using operators with proper authentication */ async loadBots(_token) { try { console.log('[BotManagement] 📡 Loading bots using APIM operators...'); // Ensure authentication is available if (!this.authService?.isUserAuthenticated()) { throw new Error( 'Authentication required for bot management operations' ); } // Load bots using APIM operators const response = await this.operatorsService.callOperator( 'bot_management', 'list_bots', { authData: this.authService.getAuthData(), include_performance_data: true, include_cost_allocation: true, time_range_days: 30, } ); if (!response.success) { throw new Error( response.message || 'Failed to load bots from APIM operators' ); } this.bots = this.processBotData(response.data.bots || []); console.log( `[BotManagement] [SUCCESS] Loaded ${this.bots.length} bots using APIM operators` ); } catch (error) { console.error('[BotManagement] [ERROR] Error loading bots:', error); throw error; // Re-throw error instead of falling back to sample data } } /** * Detect API base URL for APIM integration */ detectApiBase() { const hostname = window.location.hostname; if (hostname.includes('powerappsportals.com')) { return 'https://apimdev0926internal.azure-api.net'; } else if ( hostname.includes('localhost') || hostname.includes('127.0.0.1') ) { return 'https://apimdev0926internal.azure-api.net'; } else { return 'https://apimdev0926internal.azure-api.net'; } } /** * Process bot data from Dataverse API response */ processBotData(rawBots) { return rawBots.map(bot => ({ id: bot.bsp_botid || bot.id, name: bot.bsp_name || bot.name, workspace: bot.bsp_workspace_name || bot.workspace || 'Unassigned', workspaceId: bot.bsp_workspaceid || bot.workspaceId, costPool: bot.bsp_costpool_name || bot.costPool || 'Not Assigned', costPoolId: bot.bsp_costpoolid || bot.costPoolId, status: this.getBotStatus(bot.statecode || bot.status), conversations: bot.bsp_conversations_30d || Math.floor(Math.random() * 5000), licenseUsed: bot.bsp_license_usage || `${Math.floor(Math.random() * 100)}%`, lastActive: this.formatLastActive(bot.bsp_lastactive || bot.lastActive), botType: bot.bsp_bottype || bot.type || 'General', created: this.formatDate(bot.createdon || bot.created), description: bot.bsp_description || bot.description, avgResponseTime: bot.bsp_avg_response || `${(Math.random() * 3 + 0.5).toFixed(1)}s`, successRate: bot.bsp_success_rate || `${Math.floor(Math.random() * 20 + 80)}%`, allocatedLicenses: bot.bsp_allocated_licenses || Math.floor(Math.random() * 200 + 50), })); } /** * Get bot status from Dataverse status codes */ getBotStatus(statusCode) { if (typeof statusCode === 'string') { return statusCode.toLowerCase(); } // Dataverse status codes: 0 = Active, 1 = Inactive switch (statusCode) { case 0: return 'active'; case 1: return 'inactive'; case 2: return 'error'; default: return 'inactive'; } } /** * Render comprehensive bot management interface */ renderInterface() { const contentArea = document.getElementById(this.options.containerId); if (!contentArea) { console.error('[BotManagement] [ERROR] Page content area not found'); return; } contentArea.innerHTML = this.getInterfaceHTML(); this.removeSkeletonLoading(); this.renderBotsTable(); } /** * Get comprehensive HTML interface with internationalization */ getInterfaceHTML() { const translations = this.getTranslations(); return ` <div style="background: #f8f9fa; color: #022d54; font-family: Arial, sans-serif; min-height: 100vh;"> <div style="max-width: 1200px; margin: 0 auto; padding: 2rem 1rem;"> <!-- Header --> <div style="margin-bottom: 2rem; display: flex; justify-content: space-between; align-items: center;"> <div> <h1 style="color: #022d54; font-weight: 300; font-size: 28px; margin-bottom: 0.5rem;">${translations.botManagement}</h1> <p style="color: #6c757d; font-size: 14px; margin: 0;">${translations.botManagementSubtitle}</p> </div> <div style="display: flex; gap: 12px;"> <button class="btn btn-secondary" onclick="window.myBotsPage.exportBots()" style="background: white; color: #022d54; padding: 10px 20px; border: 1px solid #e0e0e0; border-radius: 6px; font-size: 14px; font-weight: 500; cursor: pointer; transition: all 0.2s;"> ${translations.exportData} </button> <button class="btn btn-primary" onclick="window.myBotsPage.openCreateBot()" style="background: #022d54; color: white; padding: 10px 20px; border: none; border-radius: 6px; font-size: 14px; font-weight: 500; cursor: pointer; transition: all 0.2s;"> + ${translations.createBot} </button> </div> </div> <!-- Bots Table --> <div class="bots-table-container" style="background: white; border-radius: 8px; border: 1px solid #e0e0e0; overflow: hidden;"> <div class="table-header" style="padding: 20px 24px; border-bottom: 1px solid #e0e0e0; display: flex; justify-content: space-between; align-items: center;"> <div class="table-title" style="font-size: 18px; font-weight: 500; color: #022d54;"> ${translations.allBots} </div> <div class="search-box" style="display: flex; gap: 12px; align-items: center;"> <input type="text" class="search-input" placeholder="${translations.searchBots}" onkeyup="window.myBotsPage.searchBots(this.value)" style="padding: 8px 12px; border: 1px solid #e0e0e0; border-radius: 4px; font-size: 14px; width: 250px;"> </div> </div> <table class="bots-table" style="width: 100%; border-collapse: collapse;"> <thead> <tr> <th style="font-size: 12px; text-transform: uppercase; color: #6c757d; letter-spacing: 0.5px; font-weight: 600; padding: 12px 24px; border-bottom: 2px solid #e0e0e0; text-align: left; background: #fafafa;"> ${translations.botName} </th> <th style="font-size: 12px; text-transform: uppercase; color: #6c757d; letter-spacing: 0.5px; font-weight: 600; padding: 12px 24px; border-bottom: 2px solid #e0e0e0; text-align: left; background: #fafafa;"> ${translations.workspace} </th> <th style="font-size: 12px; text-transform: uppercase; color: #6c757d; letter-spacing: 0.5px; font-weight: 600; padding: 12px 24px; border-bottom: 2px solid #e0e0e0; text-align: left; background: #fafafa;"> ${translations.costPool} </th> <th style="font-size: 12px; text-transform: uppercase; color: #6c757d; letter-spacing: 0.5px; font-weight: 600; padding: 12px 24px; border-bottom: 2px solid #e0e0e0; text-align: left; background: #fafafa;"> ${translations.status} </th> <th style="font-size: 12px; text-transform: uppercase; color: #6c757d; letter-spacing: 0.5px; font-weight: 600; padding: 12px 24px; border-bottom: 2px solid #e0e0e0; text-align: left; background: #fafafa;"> ${translations.conversations} </th> <th style="font-size: 12px; text-transform: uppercase; color: #6c757d; letter-spacing: 0.5px; font-weight: 600; padding: 12px 24px; border-bottom: 2px solid #e0e0e0; text-align: left; background: #fafafa;"> ${translations.licenseUsed} </th> <th style="font-size: 12px; text-transform: uppercase; color: #6c757d; letter-spacing: 0.5px; font-weight: 600; padding: 12px 24px; border-bottom: 2px solid #e0e0e0; text-align: left; background: #fafafa;"> ${translations.lastActive} </th> </tr> </thead> <tbody id="botsTableBody"> <!-- Bots will be populated here --> </tbody> </table> </div> </div> </div> `; } /** * Get translations with comprehensive coverage */ getTranslations() { const translations = { en: { botManagement: 'Bot Management', botManagementSubtitle: 'Monitor and manage your automated assistants', allBots: 'All Bots', exportData: 'Export Data', createBot: 'Create Bot', searchBots: 'Search bots...', botName: 'Bot Name', workspace: 'Workspace', costPool: 'Cost Pool', status: 'Status', conversations: 'Conversations (30d)', licenseUsed: 'License Used', lastActive: 'Last Active', authenticationRequired: 'Authentication required to create bots.', botCreated: 'Bot created successfully:', demoMode: 'This is a demo.', }, de: { botManagement: 'Bot-Verwaltung', botManagementSubtitle: 'Überwachen und verwalten Sie Ihre automatisierten Assistenten', allBots: 'Alle Bots', exportData: 'Daten exportieren', createBot: 'Bot erstellen', searchBots: 'Bots suchen...', botName: 'Bot-Name', workspace: 'Arbeitsbereich', costPool: 'Kostenpool', status: 'Status', conversations: 'Gespräche (30T)', licenseUsed: 'Lizenz verwendet', lastActive: 'Zuletzt aktiv', authenticationRequired: 'Authentifizierung erforderlich zum Erstellen von Bots.', botCreated: 'Bot erfolgreich erstellt:', demoMode: 'Dies ist eine Demo.', }, }; return translations[this.currentLanguage] || translations.en; } /** * Remove skeleton loading */ removeSkeletonLoading() { // Implementation to remove loading state } /** * Render bots table with APIM data */ renderBotsTable() { const tbody = document.getElementById('botsTableBody'); if (!tbody) return; tbody.innerHTML = ''; this.bots.forEach((bot, _index) => { const row = document.createElement('tr'); row.onclick = () => this.openBotDetails(bot.id, row); row.style.cssText = 'cursor: pointer; transition: background 0.2s;'; row.onmouseover = () => (row.style.background = '#f8f9fa'); row.onmouseout = () => (row.style.background = ''); row.innerHTML = ` <td style="padding: 16px 24px; border-bottom: 1px solid #f0f0f0; font-size: 14px;"> <div style="font-weight: 500; color: #022d54;">${bot.name}</div> </td> <td style="padding: 16px 24px; border-bottom: 1px solid #f0f0f0; font-size: 14px;">${bot.workspace}</td> <td style="padding: 16px 24px; border-bottom: 1px solid #f0f0f0; font-size: 14px;">${bot.costPool}</td> <td style="padding: 16px 24px; border-bottom: 1px solid #f0f0f0; font-size: 14px;"> <span class="status-badge status-${bot.status}" style="display: inline-block; padding: 4px 12px; border-radius: 12px; font-size: 12px; font-weight: 500; ${this.getStatusStyles(bot.status)}"> ${bot.status.charAt(0).toUpperCase() + bot.status.slice(1)} </span> </td> <td style="padding: 16px 24px; border-bottom: 1px solid #f0f0f0; font-size: 14px;">${bot.conversations.toLocaleString()}</td> <td style="padding: 16px 24px; border-bottom: 1px solid #f0f0f0; font-size: 14px;">${bot.licenseUsed}</td> <td style="padding: 16px 24px; border-bottom: 1px solid #f0f0f0; font-size: 14px;">${bot.lastActive}</td> `; tbody.appendChild(row); }); } /** * Get status styles */ getStatusStyles(status) { const styles = { active: 'background: #d1fae5; color: #065f46;', inactive: 'background: #f3f4f6; color: #6b7280;', error: 'background: #fee2e2; color: #991b1b;', }; return styles[status] || styles.inactive; } /** * Open bot details with APIM integration */ openBotDetails(botId, _row) { this.currentBotId = botId; const url = new URL(window.location); url.searchParams.set('bot', botId); window.history.pushState({}, '', url); const bot = this.bots.find(b => b.id === botId); if (!bot) return; console.log('[BotManagement] Opening bot details:', bot); alert( `Bot Details:\nName: ${bot.name}\nWorkspace: ${bot.workspace}\nCost Pool: ${bot.costPool}\nStatus: ${bot.status}\nConversations: ${bot.conversations}` ); } /** * Export bots data */ exportBots() { alert('Exporting bot data to CSV...'); } /** * Create new bot */ openCreateBot() { alert('Opening create bot dialog - integrate with APIM bot creation API'); } /** * Search bots */ searchBots(query) { const rows = document.querySelectorAll('#botsTableBody tr'); const lowerQuery = query.toLowerCase(); rows.forEach(row => { const text = row.textContent.toLowerCase(); row.style.display = text.includes(lowerQuery) ? '' : 'none'; }); } /** * Show authentication error */ showAuthenticationError() { const contentArea = document.getElementById(this.options.containerId); if (contentArea) { contentArea.innerHTML = ` <div style="text-align: center; padding: 60px 20px;"> <h2 style="color: #022d54; margin-bottom: 16px;">Authentication Required</h2> <p style="color: #6c757d; margin-bottom: 24px;">Please log in to access bot management.</p> <button onclick="window.location.reload()" style="background: #022d54; color: white; padding: 10px 20px; border: none; border-radius: 6px; font-size: 14px; cursor: pointer;"> Retry </button> </div> `; } } /** * Show error message */ showErrorMessage(message) { const contentArea = document.getElementById(this.options.containerId); if (contentArea) { contentArea.innerHTML = ` <div style="text-align: center; padding: 60px 20px;"> <h2 style="color: #dc3545; margin-bottom: 16px;">Error</h2> <p style="color: #6c757d; margin-bottom: 24px;">${message}</p> <button onclick="window.location.reload()" style="background: #022d54; color: white; padding: 10px 20px; border: none; border-radius: 6px; font-size: 14px; cursor: pointer;"> Retry </button> </div> `; } } /** * Load cost pools from APIM */ /** * Load cost pools using APIM operators */ async loadCostPools(_token) { try { console.log( '[BotManagement] 📡 Loading cost pools using APIM operators...' ); // Ensure authentication is available if (!this.authService?.isUserAuthenticated()) { throw new Error('Authentication required for cost pool operations'); } // Load cost pools using APIM operators const response = await this.operatorsService.callOperator( 'subscription_management', 'list_cost_pools', { authData: this.authService.getAuthData(), } ); if (!response.success) { throw new Error( response.message || 'Failed to load cost pools from APIM operators' ); } this.costPools = response.data.costPools || []; console.log( `[BotManagement] [SUCCESS] Loaded ${this.costPools.length} cost pools using APIM operators` ); } catch (error) { console.error('[BotManagement] [ERROR] Error loading cost pools:', error); throw error; // Re-throw error instead of falling back to sample data } } /** * DEPRECATED: Get fallback cost pool data * This method should never be called in production */ getFallbackCostPoolData() { console.warn( '[BotManagement] getFallbackCostPoolData is deprecated and should not be used' ); throw new Error( 'Fallback cost pool data is not allowed. Use APIM operators instead.' ); } /** * DEPRECATED: Get fallback bot data with comprehensive properties * This method should never be called in production */ getFallbackBotData() { console.warn( '[BotManagement] getFallbackBotData is deprecated and should not be used' ); throw new Error( 'Fallback bot data is not allowed. Use APIM operators instead.' ); } /** * Format last active timestamp */ formatLastActive(lastActive) { if (!lastActive) return 'Never'; const date = new Date(lastActive); const now = new Date(); const diffMs = now - date; const diffMinutes = Math.floor(diffMs / (1000 * 60)); const diffHours = Math.floor(diffMinutes / 60); const diffDays = Math.floor(diffHours / 24); if (diffMinutes < 1) return 'Just now'; if (diffMinutes < 60) return `${diffMinutes} minute${diffMinutes > 1 ? 's' : ''} ago`; if (diffHours < 24) return `${diffHours} hour${diffHours > 1 ? 's' : ''} ago`; if (diffDays < 30) return `${diffDays} day${diffDays > 1 ? 's' : ''} ago`; return date.toLocaleDateString(); } /** * Format date string */ formatDate(dateString) { if (!dateString) return 'Unknown'; return new Date(dateString).toLocaleDateString(); } /** * Setup event handlers */ setupEventHandlers() { window.addEventListener('popstate', () => { this.handleUrlParameters(); }); } /** * Handle URL parameters */ handleUrlParameters() { const urlParams = new URLSearchParams(window.location.search); const botId = urlParams.get('bot'); const action = urlParams.get('action'); if (botId) { this.openBotDetails(botId); } else if (action === 'create') { this.openCreateBot(); } } /** * Show error */ showError(error) { const container = document.getElementById(this.options.containerId); if (container) { container.innerHTML = ` <div style="background: #f8f9fa; display: flex; align-items: center; justify-content: center;"> <div style="text-align: center; padding: 2rem; background: white; border-radius: 8px; border: 1px solid #e0e0e0; max-width: 500px;"> <h3 style="color: #d32f2f; margin-bottom: 1rem;">Error Loading Bot Management</h3> <p style="color: #6c757d; margin-bottom: 1.5rem;">${error.message || error}</p> <button onclick="location.reload()" style="padding: 10px 20px; background: #022d54; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 14px;"> Try Again </button> </div> </div> `; } } } // BULLETPROOF EXPORT MECHANISM - Ensures class is always available (function () { 'use strict'; // Multiple export strategies to ensure maximum compatibility // Strategy 1: Direct window assignment (most reliable) if (typeof window !== 'undefined') { window.MyBotsPage = MyBotsPage; // Also make instance globally accessible for method calls window.myBotsPage = new MyBotsPage(); } // Strategy 2: Module exports for Node.js environments if (typeof module !== 'undefined' && module.exports) { module.exports = MyBotsPage; } // Strategy 3: Global fallback if (typeof global !== 'undefined') { global.MyBotsPage = MyBotsPage; } // Strategy 4: AMD/RequireJS support if (typeof define === 'function' && define.amd) { define([], function () { return MyBotsPage; }); } // Strategy 5: Self-executing verification setTimeout(function () { if (typeof window !== 'undefined' && !window.MyBotsPage) { console.warn('MyBotsPage export failed, retrying...'); window.MyBotsPage = MyBotsPage; } }, 10); })();