Files
proxmox/scripts/unifi/comprehensive-page-mapper.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

370 lines
14 KiB
JavaScript
Executable File
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
/**
* Comprehensive Page Mapper
*
* This script fully maps the UDM Pro routing page to understand:
* - All UI elements and their relationships
* - Page state and how it changes
* - Where buttons appear based on context
* - How scrolling and interactions affect element visibility
*/
import { chromium } from 'playwright';
import { readFileSync, writeFileSync } 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('🗺️ Comprehensive UDM Pro Page 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...');
// First ensure we're logged in and on dashboard
const currentUrl = page.url();
if (currentUrl.includes('/login')) {
console.log(' Still on login, waiting for redirect...');
await page.waitForURL('**/network/**', { timeout: 15000 }).catch(() => {});
await page.waitForTimeout(3000);
}
// Navigate to routing page
console.log(' Navigating to routing settings...');
await page.goto(`${UDM_URL}/network/default/settings/routing`, { waitUntil: 'domcontentloaded' });
await page.waitForTimeout(3000);
// Wait for URL to be correct (handle redirects)
try {
await page.waitForURL('**/settings/routing**', { timeout: 20000 });
console.log(' ✅ On routing page');
} catch (error) {
console.log(` ⚠️ URL check failed, current: ${page.url()}`);
// Try navigating again
await page.goto(`${UDM_URL}/network/default/settings/routing`, { waitUntil: 'networkidle' });
await page.waitForTimeout(5000);
}
// Verify we're on the right page
const pageText = await page.textContent('body').catch(() => '');
if (!pageText.includes('Route') && !pageText.includes('routing') && !pageText.includes('Static')) {
console.log(' ⚠️ Page may not be fully loaded, waiting more...');
await page.waitForTimeout(10000);
}
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('3. Comprehensive page mapping...\n');
// Take full page screenshot
await page.screenshot({ path: 'mapper-full-page.png', fullPage: true });
console.log(' Screenshot saved: mapper-full-page.png');
// Map page at different scroll positions
const scrollPositions = [0, 500, 1000, 1500, 2000];
const pageMaps = [];
for (const scrollY of scrollPositions) {
await page.evaluate((y) => window.scrollTo(0, y), scrollY);
await page.waitForTimeout(2000);
const map = await page.evaluate(() => {
const result = {
scrollY: window.scrollY,
viewport: { width: window.innerWidth, height: window.innerHeight },
elements: {
buttons: [],
links: [],
inputs: [],
tables: [],
sections: [],
text: [],
},
};
// Get all buttons with full context
const buttons = Array.from(document.querySelectorAll('button, [role="button"]'));
buttons.forEach((btn, index) => {
const rect = btn.getBoundingClientRect();
const styles = window.getComputedStyle(btn);
if (rect.width > 0 && rect.height > 0 && styles.display !== 'none') {
// Check if in viewport
const inViewport = rect.top >= 0 && rect.top < window.innerHeight &&
rect.left >= 0 && rect.left < window.innerWidth;
if (inViewport) {
// Get full parent hierarchy
let parent = btn;
const hierarchy = [];
for (let i = 0; i < 5; i++) {
parent = parent.parentElement;
if (!parent || parent === document.body) break;
const parentText = parent.textContent?.trim().substring(0, 100) || '';
const parentClass = parent.className || '';
hierarchy.push({
tag: parent.tagName,
class: parentClass.substring(0, 80),
text: parentText,
hasRoute: parentText.includes('Route') || parentText.includes('Static'),
hasTable: parent.tagName === 'TABLE' || parent.querySelector('table'),
});
}
result.elements.buttons.push({
index,
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 },
iconOnly: !btn.textContent?.trim() && btn.querySelector('svg'),
enabled: !btn.disabled,
visible: styles.visibility !== 'hidden',
hierarchy: hierarchy.slice(0, 3), // Top 3 parents
});
}
}
});
// Get 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 inViewport = rect.top >= 0 && rect.top < window.innerHeight;
if (inViewport) {
const headers = Array.from(table.querySelectorAll('th')).map(th => th.textContent?.trim() || '');
const rows = Array.from(table.querySelectorAll('tbody tr, tr')).length;
const tableText = table.textContent || '';
result.elements.tables.push({
index,
headers,
rowCount: rows,
hasRouteText: tableText.includes('Route') || tableText.includes('Static'),
position: { x: rect.x, y: rect.y, width: rect.width, height: rect.height },
buttonsInTable: Array.from(table.querySelectorAll('button')).length,
});
}
}
});
// Get all text content that might indicate sections
const routeTexts = [];
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) {
routeTexts.push({
text: text.substring(0, 100),
tag: parent.tagName,
className: parent.className?.substring(0, 80) || '',
position: parent.getBoundingClientRect(),
});
}
}
}
result.elements.text = routeTexts.slice(0, 20);
return result;
});
pageMaps.push(map);
}
// Analyze results
console.log('📊 Page Mapping Results:');
console.log('='.repeat(80));
// Combine all buttons found at different scroll positions
const allButtons = new Map();
pageMaps.forEach((map, scrollIndex) => {
map.elements.buttons.forEach(btn => {
const key = btn.id || btn.className || `${btn.position.x}-${btn.position.y}`;
if (!allButtons.has(key)) {
allButtons.set(key, { ...btn, foundAtScroll: [scrollIndex] });
} else {
allButtons.get(key).foundAtScroll.push(scrollIndex);
}
});
});
console.log(`\n🔘 Unique Buttons Found: ${allButtons.size}`);
Array.from(allButtons.values()).forEach((btn, i) => {
console.log(`\n${i + 1}. Button:`);
console.log(` Text: "${btn.text}"`);
console.log(` Class: ${btn.className.substring(0, 80)}`);
console.log(` ID: ${btn.id || 'none'}`);
console.log(` Position: (${btn.position.x}, ${btn.position.y})`);
console.log(` Icon Only: ${btn.iconOnly}`);
console.log(` Enabled: ${btn.enabled}`);
console.log(` Found at scroll positions: ${btn.foundAtScroll.join(', ')}`);
if (btn.hierarchy.length > 0) {
console.log(` Parent Context:`);
btn.hierarchy.forEach((parent, j) => {
console.log(` ${j + 1}. <${parent.tag}> ${parent.class} - "${parent.text.substring(0, 50)}"`);
console.log(` Has Route: ${parent.hasRoute}, Has Table: ${parent.hasTable}`);
});
}
});
// Find tables
const allTables = [];
pageMaps.forEach(map => {
map.elements.tables.forEach(table => {
if (!allTables.find(t => t.index === table.index)) {
allTables.push(table);
}
});
});
console.log(`\n📋 Tables Found: ${allTables.length}`);
allTables.forEach((table, i) => {
console.log(`\n${i + 1}. Table ${table.index}:`);
console.log(` Headers: ${table.headers.join(', ')}`);
console.log(` Rows: ${table.rowCount}`);
console.log(` Has Route Text: ${table.hasRouteText}`);
console.log(` Buttons in Table: ${table.buttonsInTable}`);
console.log(` Position: (${table.position.x}, ${table.position.y})`);
});
// Find route-related text
const routeTexts = [];
pageMaps.forEach(map => {
map.elements.text.forEach(text => {
if (!routeTexts.find(t => t.text === text.text)) {
routeTexts.push(text);
}
});
});
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})`);
});
// Save full map to file
const mapData = {
url: page.url(),
timestamp: new Date().toISOString(),
scrollMaps: pageMaps,
allButtons: Array.from(allButtons.values()),
allTables,
routeTexts,
};
writeFileSync('page-map.json', JSON.stringify(mapData, null, 2));
console.log('\n💾 Full page map saved to: page-map.json');
// Identify most likely Add button
console.log(`\n🎯 Most Likely Add Button Candidates:`);
console.log('='.repeat(80));
const candidates = Array.from(allButtons.values())
.filter(btn => btn.iconOnly || btn.text.toLowerCase().includes('add') || btn.text.toLowerCase().includes('create'))
.sort((a, b) => {
// Prioritize buttons with route context
const aHasRoute = a.hierarchy.some(p => p.hasRoute);
const bHasRoute = b.hierarchy.some(p => p.hasRoute);
if (aHasRoute && !bHasRoute) return -1;
if (!aHasRoute && bHasRoute) return 1;
// Then prioritize buttons near tables
const aNearTable = a.hierarchy.some(p => p.hasTable);
const bNearTable = b.hierarchy.some(p => p.hasTable);
if (aNearTable && !bNearTable) return -1;
if (!aNearTable && bNearTable) return 1;
return 0;
});
candidates.slice(0, 5).forEach((btn, i) => {
console.log(`\n${i + 1}. "${btn.text}" - ${btn.className.substring(0, 60)}`);
console.log(` ID: ${btn.id || 'none'}`);
console.log(` Has Route Context: ${btn.hierarchy.some(p => p.hasRoute)}`);
console.log(` Near Table: ${btn.hierarchy.some(p => p.hasTable)}`);
console.log(` Selector: ${btn.id ? `#${btn.id}` : `.${btn.className.split(' ')[0]}`}`);
});
console.log('\n\n⏸ Page is open in browser. Inspect manually if needed.');
console.log('Press Ctrl+C to close...\n');
await page.waitForTimeout(60000);
} catch (error) {
console.error('❌ Error:', error.message);
await page.screenshot({ path: 'mapper-error.png', fullPage: true });
} finally {
await browser.close();
}
})();