Files
proxmox/scripts/unifi/map-routing-page-structure.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

334 lines
13 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
/**
* 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();
}
})();