#!/usr/bin/env node /** * Map Routing Page Structure * * This script fully maps the UDM Pro routing page structure to understand: * - All sections and their locations * - Button placements and contexts * - How the page changes based on state * - Form locations and structures */ import { chromium } from 'playwright'; import { readFileSync } from 'fs'; import { join } from 'path'; import { homedir } from 'os'; // Load environment variables const envPath = join(homedir(), '.env'); function loadEnvFile(filePath) { try { const envFile = readFileSync(filePath, 'utf8'); const envVars = envFile.split('\n').filter( (line) => line.includes('=') && !line.trim().startsWith('#') ); for (const line of envVars) { const [key, ...values] = line.split('='); if (key && values.length > 0 && /^[A-Z_][A-Z0-9_]*$/.test(key.trim())) { let value = values.join('=').trim(); if ( (value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'")) ) { value = value.slice(1, -1); } process.env[key.trim()] = value; } } return true; } catch { return false; } } loadEnvFile(envPath); const UDM_URL = process.env.UNIFI_UDM_URL || 'https://192.168.0.1'; const USERNAME = process.env.UNIFI_BROWSER_USERNAME || process.env.UNIFI_USERNAME || 'unifi_api'; const PASSWORD = process.env.UNIFI_BROWSER_PASSWORD || process.env.UNIFI_PASSWORD; console.log('πŸ—ΊοΈ UDM Pro Routing Page Structure Mapper'); console.log('==========================================\n'); if (!PASSWORD) { console.error('❌ UNIFI_PASSWORD must be set in ~/.env'); process.exit(1); } (async () => { const browser = await chromium.launch({ headless: false }); const context = await browser.newContext({ ignoreHTTPSErrors: true }); const page = await context.newPage(); try { console.log('1. Logging in...'); await page.goto(UDM_URL, { waitUntil: 'networkidle' }); await page.waitForSelector('input[type="text"]'); await page.fill('input[type="text"]', USERNAME); await page.fill('input[type="password"]', PASSWORD); await page.click('button[type="submit"]'); await page.waitForTimeout(5000); console.log('2. Navigating to Routing page...'); // Wait for dashboard to load first await page.waitForTimeout(3000); // Navigate to routing page await page.goto(`${UDM_URL}/network/default/settings/routing`, { waitUntil: 'networkidle' }); await page.waitForTimeout(5000); // Wait for routing-specific content await page.waitForSelector('body', { timeout: 10000 }); // Check if we're actually on the routing page const currentUrl = page.url(); console.log(` Current URL: ${currentUrl}`); if (currentUrl.includes('/login')) { console.log(' ⚠️ Still on login page, waiting for redirect...'); await page.waitForURL('**/settings/routing**', { timeout: 15000 }).catch(() => {}); await page.waitForTimeout(5000); } // Wait for API calls to complete await page.waitForResponse(response => response.url().includes('/rest/routing') || response.url().includes('/settings/routing'), { timeout: 10000 } ).catch(() => {}); await page.waitForTimeout(5000); // Final wait for full render console.log('3. Mapping page structure...\n'); // Get comprehensive page structure const pageStructure = await page.evaluate(() => { const structure = { url: window.location.href, title: document.title, sections: [], buttons: [], forms: [], tables: [], textContent: {}, layout: {}, }; // Find all major sections const sectionSelectors = [ 'main', 'section', '[role="main"]', '[class*="container" i]', '[class*="section" i]', '[class*="panel" i]', '[class*="content" i]', '[class*="page" i]', ]; sectionSelectors.forEach(selector => { const elements = document.querySelectorAll(selector); elements.forEach((el, index) => { const rect = el.getBoundingClientRect(); if (rect.width > 100 && rect.height > 100) { const text = el.textContent?.trim().substring(0, 200) || ''; structure.sections.push({ selector, index, tag: el.tagName, className: el.className || '', id: el.id || '', text: text, position: { x: rect.x, y: rect.y, width: rect.width, height: rect.height }, hasButtons: el.querySelectorAll('button').length, hasForms: el.querySelectorAll('form').length, hasTables: el.querySelectorAll('table').length, }); } }); }); // Find all buttons with full context const buttons = Array.from(document.querySelectorAll('button, [role="button"], a[class*="button" i]')); buttons.forEach((btn, index) => { const rect = btn.getBoundingClientRect(); if (rect.width > 0 && rect.height > 0) { const styles = window.getComputedStyle(btn); if (styles.display !== 'none' && styles.visibility !== 'hidden') { // Get parent context let parent = btn.parentElement; let parentContext = ''; let depth = 0; while (parent && depth < 5) { const parentText = parent.textContent?.trim() || ''; const parentClass = parent.className || ''; if (parentText.length > 0 && parentText.length < 100) { parentContext = parentText + ' > ' + parentContext; } if (parentClass.includes('route') || parentClass.includes('routing') || parentClass.includes('table') || parentClass.includes('header')) { parentContext = `[${parentClass.substring(0, 50)}] > ` + parentContext; } parent = parent.parentElement; depth++; } structure.buttons.push({ index, tag: btn.tagName, text: btn.textContent?.trim() || '', className: btn.className || '', id: btn.id || '', ariaLabel: btn.getAttribute('aria-label') || '', dataTestId: btn.getAttribute('data-testid') || '', position: { x: rect.x, y: rect.y, width: rect.width, height: rect.height }, parentContext: parentContext.substring(0, 200), isVisible: true, isEnabled: !btn.disabled, hasIcon: btn.querySelector('svg') !== null, iconOnly: !btn.textContent?.trim() && btn.querySelector('svg') !== null, }); } } }); // Find all forms const forms = Array.from(document.querySelectorAll('form')); forms.forEach((form, index) => { const rect = form.getBoundingClientRect(); if (rect.width > 0 && rect.height > 0) { const inputs = Array.from(form.querySelectorAll('input, select, textarea')); structure.forms.push({ index, id: form.id || '', className: form.className || '', action: form.action || '', method: form.method || '', inputs: inputs.map(input => ({ type: input.type || input.tagName, name: input.name || '', placeholder: input.placeholder || '', id: input.id || '', })), position: { x: rect.x, y: rect.y, width: rect.width, height: rect.height }, }); } }); // Find all tables const tables = Array.from(document.querySelectorAll('table')); tables.forEach((table, index) => { const rect = table.getBoundingClientRect(); if (rect.width > 0 && rect.height > 0) { const headers = Array.from(table.querySelectorAll('th')).map(th => th.textContent?.trim() || ''); const rows = Array.from(table.querySelectorAll('tbody tr')).length; structure.tables.push({ index, className: table.className || '', id: table.id || '', headers, rowCount: rows, position: { x: rect.x, y: rect.y, width: rect.width, height: rect.height }, hasButtons: table.querySelectorAll('button').length, }); } }); // Get page text content for context const bodyText = document.body.textContent || ''; structure.textContent = { hasStaticRoutes: bodyText.includes('Static Routes') || bodyText.includes('Static Route'), hasRoutes: bodyText.includes('Route') && !bodyText.includes('Router'), hasAdd: bodyText.includes('Add') || bodyText.includes('Create') || bodyText.includes('New'), hasTable: bodyText.includes('table') || document.querySelector('table') !== null, fullText: bodyText.substring(0, 1000), }; return structure; }); console.log('πŸ“„ Page Structure:'); console.log('='.repeat(80)); console.log(`URL: ${pageStructure.url}`); console.log(`Title: ${pageStructure.title}`); console.log(`\nText Context:`); console.log(` Has "Static Routes": ${pageStructure.textContent.hasStaticRoutes}`); console.log(` Has "Route": ${pageStructure.textContent.hasRoutes}`); console.log(` Has "Add/Create": ${pageStructure.textContent.hasAdd}`); console.log(` Has Table: ${pageStructure.textContent.hasTable}`); console.log(`\nπŸ“¦ Sections (${pageStructure.sections.length}):`); pageStructure.sections.forEach((section, i) => { console.log(`\n${i + 1}. ${section.tag}.${section.className.substring(0, 50)}`); console.log(` Position: (${section.position.x}, ${section.position.y}) ${section.position.width}x${section.position.height}`); console.log(` Buttons: ${section.hasButtons}, Forms: ${section.hasForms}, Tables: ${section.hasTables}`); console.log(` Text: "${section.text.substring(0, 100)}"`); }); console.log(`\nπŸ”˜ Buttons (${pageStructure.buttons.length}):`); pageStructure.buttons.forEach((btn, i) => { console.log(`\n${i + 1}. Button ${btn.index}:`); console.log(` Tag: ${btn.tag}`); console.log(` Text: "${btn.text}"`); console.log(` Class: ${btn.className.substring(0, 80)}`); console.log(` ID: ${btn.id || 'none'}`); console.log(` Aria Label: ${btn.ariaLabel || 'none'}`); console.log(` Position: (${btn.position.x}, ${btn.position.y}) ${btn.position.width}x${btn.position.height}`); console.log(` Icon Only: ${btn.iconOnly}, Has Icon: ${btn.hasIcon}`); console.log(` Enabled: ${btn.isEnabled}`); console.log(` Context: ${btn.parentContext.substring(0, 150)}`); }); console.log(`\nπŸ“‹ Tables (${pageStructure.tables.length}):`); pageStructure.tables.forEach((table, i) => { console.log(`\n${i + 1}. Table ${table.index}:`); console.log(` Class: ${table.className.substring(0, 80)}`); console.log(` Headers: ${table.headers.join(', ')}`); console.log(` Rows: ${table.rowCount}`); console.log(` Buttons: ${table.hasButtons}`); console.log(` Position: (${table.position.x}, ${table.position.y})`); }); console.log(`\nπŸ“ Forms (${pageStructure.forms.length}):`); pageStructure.forms.forEach((form, i) => { console.log(`\n${i + 1}. Form ${form.index}:`); console.log(` Inputs: ${form.inputs.length}`); form.inputs.forEach((input, j) => { console.log(` ${j + 1}. ${input.type} name="${input.name}" placeholder="${input.placeholder}"`); }); }); // Identify potential Add button console.log(`\n🎯 Potential Add Button Analysis:`); console.log('='.repeat(80)); const iconOnlyButtons = pageStructure.buttons.filter(b => b.iconOnly); const buttonsNearRoutes = pageStructure.buttons.filter(b => b.parentContext.toLowerCase().includes('route') || b.parentContext.toLowerCase().includes('routing') || b.parentContext.toLowerCase().includes('table') ); console.log(`Icon-only buttons: ${iconOnlyButtons.length}`); iconOnlyButtons.forEach((btn, i) => { console.log(` ${i + 1}. ${btn.className.substring(0, 60)} - Position: (${btn.position.x}, ${btn.position.y})`); }); console.log(`\nButtons near routes/table: ${buttonsNearRoutes.length}`); buttonsNearRoutes.forEach((btn, i) => { console.log(` ${i + 1}. "${btn.text}" - ${btn.className.substring(0, 60)}`); }); console.log('\n\n⏸️ Page is open in browser. Inspect manually if needed.'); console.log('Press Ctrl+C to close...\n'); // Keep browser open await page.waitForTimeout(60000); } catch (error) { console.error('❌ Error:', error.message); await page.screenshot({ path: 'map-error.png', fullPage: true }); } finally { await browser.close(); } })();