Files
proxmox/scripts/unifi/analyze-page-visually.js
defiQUG fbda1b4beb
Some checks failed
Deploy to Phoenix / deploy (push) Has been cancelled
docs: Ledger Live integration, contract deploy learnings, NEXT_STEPS updates
- 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>
2026-02-12 15:46:57 -08:00

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();
}
})();