#!/usr/bin/env node /** * Inspect Routing Page - Find Add Button * * This script opens the UDM Pro routing page and inspects all elements * to help identify the Add button selector. */ 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 Inspector'); 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(3000); console.log('2. Navigating to Routing page...'); await page.goto(`${UDM_URL}/network/default/settings/routing`, { waitUntil: 'networkidle' }); await page.waitForTimeout(5000); console.log('3. Inspecting page elements...\n'); // Get all clickable elements const clickableElements = await page.evaluate(() => { const elements = []; const allElements = document.querySelectorAll('button, a, [role="button"], [onclick], [class*="button"], [class*="click"]'); allElements.forEach((el, index) => { const rect = el.getBoundingClientRect(); if (rect.width > 0 && rect.height > 0) { const styles = window.getComputedStyle(el); if (styles.display !== 'none' && styles.visibility !== 'hidden') { const text = el.textContent?.trim() || ''; const tagName = el.tagName.toLowerCase(); const className = el.className || ''; const id = el.id || ''; const ariaLabel = el.getAttribute('aria-label') || ''; const dataTestId = el.getAttribute('data-testid') || ''; const onclick = el.getAttribute('onclick') || ''; // Get parent info const parent = el.parentElement; const parentClass = parent?.className || ''; const parentTag = parent?.tagName.toLowerCase() || ''; elements.push({ index, tagName, text: text.substring(0, 50), className: className.substring(0, 100), id, ariaLabel, dataTestId, onclick: onclick.substring(0, 50), parentTag, parentClass: parentClass.substring(0, 100), xpath: getXPath(el), }); } } }); function getXPath(element) { if (element.id !== '') { return `//*[@id="${element.id}"]`; } if (element === document.body) { return '/html/body'; } let ix = 0; const siblings = element.parentNode.childNodes; for (let i = 0; i < siblings.length; i++) { const sibling = siblings[i]; if (sibling === element) { return getXPath(element.parentNode) + '/' + element.tagName.toLowerCase() + '[' + (ix + 1) + ']'; } if (sibling.nodeType === 1 && sibling.tagName === element.tagName) { ix++; } } } return elements; }); console.log(`Found ${clickableElements.length} clickable elements:\n`); // Filter for potential Add buttons const potentialAddButtons = clickableElements.filter(el => { const text = el.text.toLowerCase(); const className = el.className.toLowerCase(); const ariaLabel = el.ariaLabel.toLowerCase(); return ( text.includes('add') || text.includes('create') || text.includes('new') || text === '+' || text === '' || className.includes('add') || className.includes('create') || ariaLabel.includes('add') || ariaLabel.includes('create') ); }); console.log('🎯 Potential Add Buttons:'); console.log('='.repeat(80)); potentialAddButtons.forEach((el, i) => { console.log(`\n${i + 1}. Element ${el.index}:`); console.log(` Tag: ${el.tagName}`); console.log(` Text: "${el.text}"`); console.log(` Class: ${el.className}`); console.log(` ID: ${el.id || 'none'}`); console.log(` Aria Label: ${el.ariaLabel || 'none'}`); console.log(` Data Test ID: ${el.dataTestId || 'none'}`); console.log(` Parent: <${el.parentTag}> ${el.parentClass}`); console.log(` XPath: ${el.xpath}`); console.log(` Selector: ${el.tagName}${el.id ? `#${el.id}` : ''}${el.className ? `.${el.className.split(' ')[0]}` : ''}`); }); console.log('\n\nπŸ“‹ All Buttons on Page:'); console.log('='.repeat(80)); const buttons = clickableElements.filter(el => el.tagName === 'button'); buttons.forEach((el, i) => { console.log(`\n${i + 1}. Button ${el.index}:`); console.log(` Text: "${el.text}"`); console.log(` Class: ${el.className}`); console.log(` Aria Label: ${el.ariaLabel || 'none'}`); console.log(` XPath: ${el.xpath}`); }); console.log('\n\n⏸️ Page is open in browser. Inspect elements 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: 'inspect-error.png', fullPage: true }); } finally { await browser.close(); } })();