Some checks failed
Deploy to Phoenix / deploy (push) Has been cancelled
- ADD_CHAIN138_TO_LEDGER_LIVE: Ledger form done; public code review repo bis-innovations/LedgerLive; init/push commands - CONTRACT_DEPLOYMENT_RUNBOOK: Chain 138 gas price 1 gwei, 36-addr check, TransactionMirror workaround - CONTRACT_*: AddressMapper, MirrorManager deployed 2026-02-12; 36-address on-chain check - NEXT_STEPS_FOR_YOU: Ledger done; steps completable now (no LAN); run-completable-tasks-from-anywhere - MASTER_INDEX, OPERATOR_OPTIONAL, SMART_CONTRACTS_INVENTORY_SIMPLE: updates - LEDGER_BLOCKCHAIN_INTEGRATION_COMPLETE: bis-innovations/LedgerLive reference Co-authored-by: Cursor <cursoragent@cursor.com>
333 lines
12 KiB
JavaScript
Executable File
333 lines
12 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
/**
|
|
* Visual Page Analyzer
|
|
*
|
|
* This script opens the routing page in a visible browser and provides
|
|
* interactive analysis tools to identify the Add button location.
|
|
*/
|
|
|
|
import { chromium } from 'playwright';
|
|
import { readFileSync } from 'fs';
|
|
import { join } from 'path';
|
|
import { homedir } from 'os';
|
|
import readline from 'readline';
|
|
|
|
// 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('🔍 Visual Page Analyzer');
|
|
console.log('======================\n');
|
|
|
|
if (!PASSWORD) {
|
|
console.error('❌ UNIFI_PASSWORD must be set in ~/.env');
|
|
process.exit(1);
|
|
}
|
|
|
|
const rl = readline.createInterface({
|
|
input: process.stdin,
|
|
output: process.stdout
|
|
});
|
|
|
|
function question(prompt) {
|
|
return new Promise((resolve) => {
|
|
rl.question(prompt, resolve);
|
|
});
|
|
}
|
|
|
|
(async () => {
|
|
const browser = await chromium.launch({ headless: false, slowMo: 100 });
|
|
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...');
|
|
await page.goto(`${UDM_URL}/network/default/settings/routing`, { waitUntil: 'networkidle' });
|
|
await page.waitForTimeout(10000);
|
|
|
|
// Wait for URL to be correct
|
|
await page.waitForURL('**/settings/routing**', { timeout: 20000 }).catch(() => {});
|
|
await page.waitForTimeout(5000);
|
|
|
|
console.log(` Current URL: ${page.url()}`);
|
|
|
|
// Wait for routes API
|
|
try {
|
|
await page.waitForResponse(response =>
|
|
response.url().includes('/rest/routing') || response.url().includes('/trafficroutes'),
|
|
{ timeout: 15000 }
|
|
);
|
|
console.log(' Routes API loaded');
|
|
} catch (error) {
|
|
console.log(' Routes API not detected');
|
|
}
|
|
|
|
await page.waitForTimeout(5000);
|
|
|
|
console.log('\n3. Page Analysis Tools Available:');
|
|
console.log('='.repeat(80));
|
|
|
|
// Highlight all buttons
|
|
await page.evaluate(() => {
|
|
const buttons = Array.from(document.querySelectorAll('button, [role="button"]'));
|
|
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') {
|
|
// Add highlight
|
|
btn.style.outline = '3px solid red';
|
|
btn.style.outlineOffset = '2px';
|
|
btn.setAttribute('data-analyzer-index', index);
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
// Get button information
|
|
const buttonInfo = await page.evaluate(() => {
|
|
const buttons = Array.from(document.querySelectorAll('button, [role="button"]'));
|
|
return buttons.map((btn, index) => {
|
|
const rect = btn.getBoundingClientRect();
|
|
const styles = window.getComputedStyle(btn);
|
|
if (rect.width > 0 && rect.height > 0 && styles.display !== 'none') {
|
|
return {
|
|
index,
|
|
text: btn.textContent?.trim() || '',
|
|
className: btn.className || '',
|
|
id: btn.id || '',
|
|
ariaLabel: btn.getAttribute('aria-label') || '',
|
|
dataTestId: btn.getAttribute('data-testid') || '',
|
|
position: { x: Math.round(rect.x), y: Math.round(rect.y), width: Math.round(rect.width), height: Math.round(rect.height) },
|
|
iconOnly: !btn.textContent?.trim() && btn.querySelector('svg') !== null,
|
|
enabled: !btn.disabled,
|
|
selector: btn.id ? `#${btn.id}` : `.${btn.className.split(' ')[0]}`,
|
|
};
|
|
}
|
|
return null;
|
|
}).filter(b => b !== null);
|
|
});
|
|
|
|
console.log(`\n📊 Found ${buttonInfo.length} buttons on page:`);
|
|
buttonInfo.forEach((btn, i) => {
|
|
console.log(`\n${i + 1}. Button ${btn.index}:`);
|
|
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}`);
|
|
console.log(` Enabled: ${btn.enabled}`);
|
|
console.log(` Selector: ${btn.selector}`);
|
|
});
|
|
|
|
// Highlight tables
|
|
await page.evaluate(() => {
|
|
const tables = Array.from(document.querySelectorAll('table'));
|
|
tables.forEach((table, index) => {
|
|
table.style.outline = '3px solid blue';
|
|
table.style.outlineOffset = '2px';
|
|
table.setAttribute('data-analyzer-table-index', index);
|
|
});
|
|
});
|
|
|
|
const tableInfo = await page.evaluate(() => {
|
|
const tables = Array.from(document.querySelectorAll('table'));
|
|
return tables.map((table, index) => {
|
|
const rect = table.getBoundingClientRect();
|
|
const headers = Array.from(table.querySelectorAll('th')).map(th => th.textContent?.trim() || '');
|
|
const rows = Array.from(table.querySelectorAll('tbody tr, tr')).length;
|
|
return {
|
|
index,
|
|
headers,
|
|
rowCount: rows,
|
|
position: { x: Math.round(rect.x), y: Math.round(rect.y), width: Math.round(rect.width), height: Math.round(rect.height) },
|
|
buttonsInTable: Array.from(table.querySelectorAll('button')).length,
|
|
};
|
|
});
|
|
});
|
|
|
|
if (tableInfo.length > 0) {
|
|
console.log(`\n📋 Found ${tableInfo.length} tables:`);
|
|
tableInfo.forEach((table, i) => {
|
|
console.log(`\n${i + 1}. Table ${table.index}:`);
|
|
console.log(` Headers: ${table.headers.join(', ')}`);
|
|
console.log(` Rows: ${table.rowCount}`);
|
|
console.log(` Buttons in Table: ${table.buttonsInTable}`);
|
|
console.log(` Position: (${table.position.x}, ${table.position.y})`);
|
|
});
|
|
} else {
|
|
console.log('\n📋 No tables found on page');
|
|
}
|
|
|
|
// Find route-related text
|
|
const routeTexts = await page.evaluate(() => {
|
|
const texts = [];
|
|
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
|
|
let node;
|
|
while (node = walker.nextNode()) {
|
|
const text = node.textContent?.trim() || '';
|
|
if (text && (text.includes('Static Routes') || text.includes('Route') || text.includes('Add'))) {
|
|
const parent = node.parentElement;
|
|
if (parent) {
|
|
const rect = parent.getBoundingClientRect();
|
|
texts.push({
|
|
text: text.substring(0, 100),
|
|
tag: parent.tagName,
|
|
className: parent.className?.substring(0, 80) || '',
|
|
position: { x: Math.round(rect.x), y: Math.round(rect.y) },
|
|
});
|
|
}
|
|
}
|
|
}
|
|
return texts.slice(0, 20);
|
|
});
|
|
|
|
if (routeTexts.length > 0) {
|
|
console.log(`\n📝 Route-Related Text Found (${routeTexts.length}):`);
|
|
routeTexts.forEach((text, i) => {
|
|
console.log(`\n${i + 1}. "${text.text}"`);
|
|
console.log(` Tag: ${text.tag}, Class: ${text.className}`);
|
|
console.log(` Position: (${text.position.x}, ${text.position.y})`);
|
|
});
|
|
}
|
|
|
|
console.log('\n\n🎯 Interactive Testing:');
|
|
console.log('='.repeat(80));
|
|
console.log('Buttons are highlighted in RED');
|
|
console.log('Tables are highlighted in BLUE');
|
|
console.log('\nYou can now:');
|
|
console.log('1. Visually inspect the page in the browser');
|
|
console.log('2. Test clicking buttons to see what they do');
|
|
console.log('3. Identify the Add Route button location');
|
|
console.log('\nPress Enter to test clicking buttons, or type "exit" to close...\n');
|
|
|
|
let testing = true;
|
|
while (testing) {
|
|
const input = await question('Enter button number to test (1-' + buttonInfo.length + '), "screenshot" to save, or "exit": ');
|
|
|
|
if (input.toLowerCase() === 'exit') {
|
|
testing = false;
|
|
break;
|
|
}
|
|
|
|
if (input.toLowerCase() === 'screenshot') {
|
|
await page.screenshot({ path: 'analyzer-screenshot.png', fullPage: true });
|
|
console.log('✅ Screenshot saved: analyzer-screenshot.png');
|
|
continue;
|
|
}
|
|
|
|
const buttonNum = parseInt(input);
|
|
if (buttonNum >= 1 && buttonNum <= buttonInfo.length) {
|
|
const btn = buttonInfo[buttonNum - 1];
|
|
console.log(`\nTesting button ${buttonNum}: "${btn.text}"`);
|
|
console.log(`Selector: ${btn.selector}`);
|
|
|
|
try {
|
|
// Highlight the button
|
|
await page.evaluate((index) => {
|
|
const btn = document.querySelector(`[data-analyzer-index="${index}"]`);
|
|
if (btn) {
|
|
btn.style.backgroundColor = 'yellow';
|
|
btn.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
}
|
|
}, btn.index);
|
|
|
|
await page.waitForTimeout(1000);
|
|
|
|
// Try clicking
|
|
const selector = btn.id ? `#${btn.id}` : `button:nth-of-type(${btn.index + 1})`;
|
|
await page.click(selector, { timeout: 5000 }).catch(async (error) => {
|
|
console.log(` ⚠️ Regular click failed: ${error.message}`);
|
|
// Try JavaScript click
|
|
await page.evaluate((index) => {
|
|
const btn = document.querySelector(`[data-analyzer-index="${index}"]`);
|
|
if (btn) btn.click();
|
|
}, btn.index);
|
|
});
|
|
|
|
await page.waitForTimeout(3000);
|
|
|
|
// Check for form
|
|
const hasForm = await page.locator('input[name="name"], input[name="destination"], input[placeholder*="destination" i]').first().isVisible({ timeout: 2000 }).catch(() => false);
|
|
if (hasForm) {
|
|
console.log(' ✅✅✅ FORM APPEARED! This is the Add button! ✅✅✅');
|
|
console.log(` Use selector: ${btn.selector}`);
|
|
console.log(` Or ID: ${btn.id || 'none'}`);
|
|
console.log(` Or class: ${btn.className.split(' ')[0]}`);
|
|
testing = false;
|
|
break;
|
|
} else {
|
|
// Check for menu
|
|
const hasMenu = await page.locator('[role="menu"], [role="listbox"]').first().isVisible({ timeout: 2000 }).catch(() => false);
|
|
if (hasMenu) {
|
|
console.log(' ⚠️ Menu appeared (not form)');
|
|
const menuItems = await page.evaluate(() => {
|
|
const menu = document.querySelector('[role="menu"], [role="listbox"]');
|
|
if (!menu) return [];
|
|
return Array.from(menu.querySelectorAll('[role="menuitem"], [role="option"], li, div')).map(item => ({
|
|
text: item.textContent?.trim() || '',
|
|
tag: item.tagName,
|
|
})).filter(item => item.text.length > 0);
|
|
});
|
|
console.log(` Menu items: ${menuItems.map(m => `"${m.text}"`).join(', ')}`);
|
|
} else {
|
|
console.log(' ❌ No form or menu appeared');
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.log(` ❌ Error: ${error.message}`);
|
|
}
|
|
} else {
|
|
console.log('Invalid button number');
|
|
}
|
|
}
|
|
|
|
console.log('\n✅ Analysis complete. Closing browser...');
|
|
|
|
} catch (error) {
|
|
console.error('❌ Error:', error.message);
|
|
await page.screenshot({ path: 'analyzer-error.png', fullPage: true });
|
|
} finally {
|
|
rl.close();
|
|
await browser.close();
|
|
}
|
|
})();
|