open-composition-editor.js•19.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();
}