Skip to main content
Glama
open-composition-editor.js19.3 kB
#!/usr/bin/env node /** * Composition Editor Opener Tool v5.2.0 - FULLY OPERATIONAL * Navigate to saved composition with comprehensive error handling and debugging * @version 5.2.0 (January 12, 2025) * @status FULLY OPERATIONAL - Complete workflow finalization with browser persistence * @reference JIT workflow step 7 of 7 * @milestone v5.2.0 - Enhanced navigation with detailed progress tracking */ export class CompositionEditorOpener { constructor() { this.processingStartTime = null; this.navigationLog = []; this.pageErrors = []; this.networkErrors = []; } /** * Main navigation entry point */ async openCompositionEditor(compositionUid, page) { this.processingStartTime = Date.now(); this.navigationLog = []; this.pageErrors = []; this.networkErrors = []; console.error(`[OPEN_COMPOSITION_EDITOR] Starting navigation to composition: ${compositionUid}`); try { // Step 1: Pre-navigation checks const preNavStatus = await this.performPreNavigationChecks(page); this.logNavigation('PRE_NAVIGATION_CHECK', 'Pre-navigation validation', preNavStatus.ready, preNavStatus); if (!preNavStatus.ready) { throw new Error(`Pre-navigation checks failed: ${preNavStatus.issues.join(', ')}`); } // Step 2: Construct and validate navigation URL const navigationPlan = this.constructNavigationPlan(page.url(), compositionUid); this.logNavigation('URL_CONSTRUCTION', 'Navigation URL constructed', true, navigationPlan); // Step 3: Set up error monitoring this.setupPageErrorMonitoring(page); // Step 4: Execute navigation with timeout and monitoring const navigationResult = await this.executeNavigation(page, navigationPlan); if (!navigationResult.success) { return this.createErrorResponse(navigationResult.error, page); } // Step 5: Verify Composer interface loaded await this.waitForComposerInterface(page); this.logNavigation('COMPOSER_INTERFACE', 'Composer interface detection completed', true); // Step 6: Verify composition loaded successfully const compositionStatus = await this.verifyCompositionLoaded(page, compositionUid); this.logNavigation('COMPOSITION_VERIFICATION', 'Composition load verification completed', compositionStatus.compositionLoaded, compositionStatus); // Step 7: Perform final page health assessment const pageAnalysis = await this.assessPageHealth(page); const processingTime = Date.now() - this.processingStartTime; const finalUrl = page.url(); console.error(`[OPEN_COMPOSITION_EDITOR] ✅ Navigation successful to: ${finalUrl}`); return { success: true, data: { finalUrl: finalUrl, pageTitle: pageAnalysis.title, loadTime: navigationResult.loadTime, editorReady: compositionStatus.editorReady, compositionLoaded: compositionStatus.compositionLoaded }, debug: { timestamp: new Date().toISOString(), processingTime: processingTime, navigationLog: this.navigationLog, pageAnalysis: pageAnalysis } }; } catch (error) { console.error('[OPEN_COMPOSITION_EDITOR] ❌ Navigation error:', error.message); return this.createErrorResponse({ step: 'NAVIGATION_ERROR', message: error.message, currentUrl: page ? page.url() : 'Unknown' }, page); } } /** * Perform pre-navigation validation checks */ async performPreNavigationChecks(page) { return await page.evaluate(() => { const issues = []; // Check authentication data const hasAuthData = !!localStorage.getItem('rdp-composer-user-data'); const hasProjectData = !!localStorage.getItem('rdp-composer-active-project'); if (!hasAuthData) issues.push('Missing authentication data'); if (!hasProjectData) issues.push('Missing project data'); // Check network status const networkOnline = navigator.onLine; if (!networkOnline) issues.push('Network offline'); // Check current URL is valid Composer domain const isComposerDomain = window.location.href.includes('composer.euconquisto.com'); if (!isComposerDomain) issues.push('Not on Composer domain'); return { ready: issues.length === 0, issues: issues, status: { currentUrl: window.location.href, readyState: document.readyState, hasAuthData: hasAuthData, hasProjectData: hasProjectData, networkState: networkOnline, isComposerDomain: isComposerDomain } }; }); } /** * Construct navigation plan with URL validation */ constructNavigationPlan(currentUrl, compositionUid) { // Extract base URL from current page const cleanBaseUrl = currentUrl.split('#')[0]; // Construct the composer editor path const composerPath = `#/composer/${compositionUid}`; const targetUrl = cleanBaseUrl + composerPath; // Validate URL format if (!this.isValidComposerUrl(targetUrl)) { throw new Error(`Invalid composer URL format: ${targetUrl}`); } // Validate composition UID format if (!this.isValidCompositionUID(compositionUid)) { throw new Error(`Invalid composition UID format: ${compositionUid}`); } return { baseUrl: cleanBaseUrl, composerPath: composerPath, targetUrl: targetUrl, compositionUid: compositionUid }; } /** * Set up comprehensive page error monitoring */ setupPageErrorMonitoring(page) { // Monitor JavaScript errors page.on('pageerror', (error) => { this.pageErrors.push({ type: 'javascript_error', message: error.message, stack: error.stack, timestamp: new Date().toISOString() }); console.error('[PAGE_ERROR]', error.message); }); // Monitor console errors page.on('console', (msg) => { if (msg.type() === 'error') { this.pageErrors.push({ type: 'console_error', message: msg.text(), timestamp: new Date().toISOString() }); } }); // Monitor network errors page.on('response', (response) => { if (!response.ok()) { this.networkErrors.push({ status: response.status(), url: response.url(), statusText: response.statusText(), timestamp: new Date().toISOString() }); console.error('[NETWORK_ERROR]', response.status(), response.url()); } }); this.logNavigation('ERROR_MONITORING', 'Error monitoring setup completed', true); } /** * Execute navigation with comprehensive monitoring */ async executeNavigation(page, navigationPlan) { const navigationStartTime = Date.now(); this.logNavigation('NAVIGATION_START', 'Starting navigation', true, { targetUrl: navigationPlan.targetUrl, timeout: 30000 }); try { // Execute navigation with appropriate wait strategy await page.goto(navigationPlan.targetUrl, { waitUntil: 'networkidle', timeout: 30000 }); const navigationTime = Date.now() - navigationStartTime; this.logNavigation('NAVIGATION_COMPLETE', 'Navigation completed successfully', true, { loadTime: navigationTime, finalUrl: page.url() }); return { success: true, loadTime: navigationTime, finalUrl: page.url() }; } catch (error) { const navigationTime = Date.now() - navigationStartTime; this.logNavigation('NAVIGATION_FAILED', 'Navigation failed', false, { error: error.message, loadTime: navigationTime, currentUrl: page.url() }); return { success: false, error: { step: 'NAVIGATION_EXECUTION', message: error.message, loadTime: navigationTime, currentUrl: page.url() } }; } } /** * Wait for Composer interface to be fully loaded */ async waitForComposerInterface(page) { this.logNavigation('COMPOSER_INTERFACE_WAIT', 'Waiting for Composer interface', true); try { // Wait for key Composer elements to appear await page.waitForFunction(() => { // Check for various Composer interface indicators const composerSelectors = [ '[data-testid="composer-app"]', '.composer-editor', '#composer-root', '.rdp-composer', '[data-composer]' ]; const composerElement = composerSelectors.some(selector => document.querySelector(selector) ); // Check for loading indicators to disappear const loadingSelectors = [ '.loading', '.spinner', '[data-loading="true"]', '.composer-loading' ]; const loadingElements = loadingSelectors.some(selector => document.querySelector(selector) ); // Composer should be present and loading should be done return composerElement && !loadingElements; }, { timeout: 15000 }); // Additional stabilization wait await page.waitForTimeout(2000); this.logNavigation('COMPOSER_INTERFACE_READY', 'Composer interface is ready', true); } catch (error) { this.logNavigation('COMPOSER_INTERFACE_TIMEOUT', 'Composer interface timeout', false, { error: error.message }); throw new Error(`Composer interface failed to load: ${error.message}`); } } /** * Verify composition loaded successfully */ async verifyCompositionLoaded(page, expectedUid) { this.logNavigation('COMPOSITION_VERIFICATION_START', 'Starting composition load verification', true); const status = await page.evaluate((uid) => { // Check URL contains the composition UID const urlContainsUid = window.location.href.includes(uid); // Check for composition content in the DOM const composerContentSelectors = [ '.composer-widget', '[data-widget-type]', '.widget-container', '.rdp-widget', '.composition-widget' ]; const hasComposerContent = composerContentSelectors.some(selector => document.querySelector(selector) ); // Check for error indicators const errorSelectors = [ '.error', '.not-found', '[data-error="true"]', '.composition-error', '.load-error' ]; const errorElements = document.querySelectorAll(errorSelectors.join(', ')); const hasErrors = errorElements.length > 0; // Check page title for composition indicators const titleIndicatesComposition = document.title.includes('Composer') && !document.title.includes('Error') && !document.title.includes('404'); // Check for 500/error page content const bodyText = document.body.textContent || ''; const hasErrorPageContent = bodyText.includes('500') || bodyText.includes('Internal Server Error') || bodyText.includes('Page Not Found'); return { urlContainsUid: urlContainsUid, hasComposerContent: hasComposerContent, hasErrors: hasErrors, titleIndicatesComposition: titleIndicatesComposition, hasErrorPageContent: hasErrorPageContent, pageTitle: document.title, errorMessages: Array.from(errorElements).map(el => el.textContent || '').filter(Boolean), bodyTextSample: bodyText.substring(0, 200) }; }, expectedUid); const editorReady = status.titleIndicatesComposition && !status.hasErrors && !status.hasErrorPageContent; const compositionLoaded = status.urlContainsUid && status.hasComposerContent && !status.hasErrors && !status.hasErrorPageContent; return { editorReady: editorReady, compositionLoaded: compositionLoaded, details: status }; } /** * Perform comprehensive page health assessment */ async assessPageHealth(page) { return await page.evaluate(() => { // Check for various error indicators const errorIndicators = []; // HTTP error indicators const bodyContent = document.body.innerHTML.toLowerCase(); if (bodyContent.includes('500') || bodyContent.includes('internal server error')) { errorIndicators.push('HTTP_500_ERROR'); } if (bodyContent.includes('404') || bodyContent.includes('not found')) { errorIndicators.push('HTTP_404_ERROR'); } // Authentication error indicators if (bodyContent.includes('login') || bodyContent.includes('unauthorized')) { errorIndicators.push('AUTH_ERROR_CONTENT'); } // Composer-specific error indicators if (bodyContent.includes('composition not found') || bodyContent.includes('access denied')) { errorIndicators.push('COMPOSER_ERROR_CONTENT'); } // Count loaded resources const images = document.querySelectorAll('img[src]'); const scripts = document.querySelectorAll('script[src]'); const styles = document.querySelectorAll('link[rel="stylesheet"]'); const loadedAssets = images.length + scripts.length + styles.length; // Check for Composer interface elements const composerSelectors = [ '.composer-editor', '[data-composer]', '#composer-app', '.rdp-composer' ]; const hasComposerInterface = composerSelectors.some(selector => document.querySelector(selector) ); return { url: window.location.href, title: document.title, readyState: document.readyState, hasComposerInterface: hasComposerInterface, errorIndicators: errorIndicators, loadedAssets: loadedAssets, jsErrors: [] // Populated by error monitoring }; }); } /** * URL and UID validation utilities */ isValidComposerUrl(url) { try { const urlObj = new URL(url); return urlObj.hostname.includes('composer.euconquisto.com') && url.includes('#/composer/'); } catch { return false; } } isValidCompositionUID(uid) { // Check for reasonable UID format - supports both UUID and base64-encoded formats // Base64 can contain: a-z, A-Z, 0-9, +, /, = (padding) return typeof uid === 'string' && uid.length >= 10 && uid.length <= 1000 && // Increased limit for compressed UIDs /^[a-zA-Z0-9\-_+=\/]+$/.test(uid); // Added base64 characters } /** * Create standardized error response */ createErrorResponse(errorData, page = null) { const currentUrl = page ? page.url() : 'Unknown'; const processingTime = Date.now() - this.processingStartTime; // Analyze navigation failure const analysis = this.analyzeNavigationFailure(errorData, currentUrl); return { success: false, error: { code: this.getErrorCode(errorData), message: errorData.message || 'Navigation failed', details: { step: errorData.step || 'UNKNOWN', currentUrl: currentUrl, targetUrl: errorData.targetUrl || 'Unknown', pageStatus: errorData.pageStatus || 'Unknown', consoleErrors: this.pageErrors.filter(e => e.type === 'console_error'), networkErrors: this.networkErrors, possibleCauses: analysis.possibleCauses, suggestedFixes: analysis.suggestedFixes } }, debug: { timestamp: new Date().toISOString(), processingTime: processingTime, navigationLog: this.navigationLog, pageAnalysis: errorData.pageAnalysis || null } }; } /** * Analyze navigation failure patterns */ analyzeNavigationFailure(error, currentUrl) { let possibleCauses = []; let suggestedFixes = []; // Analyze by error patterns if (error.message && error.message.includes('timeout')) { possibleCauses = [ 'Page load timeout (> 30 seconds)', 'Network connectivity issues', 'Composer server overloaded', 'Large composition causing slow load' ]; suggestedFixes = [ 'Check network connection stability', 'Retry with longer timeout', 'Verify Composer service status', 'Try accessing a different composition' ]; } else if (currentUrl.includes('status/500') || currentUrl.includes('error')) { possibleCauses = [ 'Composition UID not found in database', 'Composition data corrupted or incompatible', 'Server-side processing error', 'Database connectivity issues' ]; suggestedFixes = [ 'Verify composition UID is correct', 'Check if composition was saved successfully', 'Try creating a new composition', 'Contact support with composition UID' ]; } else if (currentUrl.includes('login') || currentUrl.includes('auth')) { possibleCauses = [ 'Authentication session expired', 'Invalid or missing JWT token', 'User permissions revoked', 'Session timeout' ]; suggestedFixes = [ 'Re-authenticate through browser refresh', 'Check JWT token file is current', 'Verify user has Composer access', 'Clear browser cache and re-login' ]; } else { possibleCauses = [ 'Unknown navigation error', 'Unexpected page state', 'Browser compatibility issue' ]; suggestedFixes = [ 'Retry navigation', 'Check browser console for errors', 'Try different browser or incognito mode' ]; } return { possibleCauses, suggestedFixes }; } /** * Determine error code from error data */ getErrorCode(errorData) { if (errorData.step === 'PRE_NAVIGATION_CHECK') return 'PRE_NAVIGATION_ERROR'; if (errorData.step === 'URL_CONSTRUCTION') return 'URL_CONSTRUCTION_ERROR'; if (errorData.step === 'NAVIGATION_EXECUTION') return 'NAVIGATION_TIMEOUT'; if (errorData.step === 'COMPOSER_INTERFACE') return 'COMPOSER_INTERFACE_ERROR'; if (errorData.step === 'COMPOSITION_VERIFICATION') return 'COMPOSITION_NOT_FOUND'; return 'NAVIGATION_ERROR'; } /** * Navigation logging utility */ logNavigation(step, details, success = true, data = null) { this.navigationLog.push({ timestamp: new Date().toISOString(), step: step, details: details, success: success, data: data }); } } /** * Create and export the editor opener component for JIT server integration */ export function createCompositionEditorOpener() { return new CompositionEditorOpener(); }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/rkm097git/euconquisto-composer-mcp-poc'

If you have feedback or need assistance with the MCP directory API, please join our Discord server