Files
proxmox/scripts/unifi/find-add-button-comprehensive.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

349 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
/**
* Comprehensive Add Button Finder
*
* This script uses multiple strategies to find the Add button:
* 1. Waits for page to fully load
* 2. Maps all page sections
* 3. Finds all tables and their associated buttons
* 4. Identifies buttons in table headers/toolbars
* 5. Tests each potential button to see what it does
*/
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('🔍 Comprehensive Add Button Finder');
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 fully load first
await page.waitForTimeout(5000);
// Navigate directly
await page.goto(`${UDM_URL}/network/default/settings/routing`, { waitUntil: 'domcontentloaded' });
// Wait for URL to change (handle redirects)
await page.waitForURL('**/settings/routing**', { timeout: 20000 }).catch(() => {
console.log(' ⚠️ URL redirect not detected, continuing...');
});
await page.waitForTimeout(5000);
// Wait for page to be interactive
await page.waitForLoadState('networkidle', { timeout: 15000 }).catch(() => {});
await page.waitForLoadState('domcontentloaded');
console.log(` Current URL: ${page.url()}`);
// Check if we're actually on routing page
const pageText = await page.textContent('body').catch(() => '');
if (!pageText.includes('Route') && !pageText.includes('routing')) {
console.log(' ⚠️ Page may not be fully loaded, waiting more...');
await page.waitForTimeout(10000);
}
// 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. Analyzing page structure...\n');
// Get comprehensive page analysis
const analysis = await page.evaluate(() => {
const result = {
tables: [],
buttons: [],
sections: [],
potentialAddButtons: [],
};
// 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, tr')).length;
const tableText = table.textContent || '';
// Find buttons in/around this table
const tableButtons = [];
let current = table;
for (let i = 0; i < 3; i++) {
const buttons = Array.from(current.querySelectorAll('button, [role="button"]'));
buttons.forEach(btn => {
const btnRect = btn.getBoundingClientRect();
if (btnRect.width > 0 && btnRect.height > 0) {
const styles = window.getComputedStyle(btn);
if (styles.display !== 'none' && styles.visibility !== 'hidden') {
tableButtons.push({
text: btn.textContent?.trim() || '',
className: btn.className || '',
id: btn.id || '',
ariaLabel: btn.getAttribute('aria-label') || '',
position: { x: btnRect.x, y: btnRect.y },
isInHeader: table.querySelector('thead')?.contains(btn) || false,
isInToolbar: btn.closest('[class*="toolbar" i], [class*="header" i], [class*="action" i]') !== null,
});
}
}
});
current = current.parentElement;
if (!current) break;
}
result.tables.push({
index,
headers,
rowCount: rows,
hasRouteText: tableText.includes('Route') || tableText.includes('Static'),
buttonCount: tableButtons.length,
buttons: tableButtons,
position: { x: rect.x, y: rect.y, width: rect.width, height: rect.height },
});
}
});
// Find all buttons with full context
const allButtons = Array.from(document.querySelectorAll('button, [role="button"]'));
allButtons.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') {
const text = btn.textContent?.trim() || '';
const className = btn.className || '';
const id = btn.id || '';
const ariaLabel = btn.getAttribute('aria-label') || '';
// Check if near a table
let nearTable = null;
let current = btn;
for (let i = 0; i < 5; i++) {
if (current.tagName === 'TABLE') {
nearTable = {
index: Array.from(document.querySelectorAll('table')).indexOf(current),
isInHeader: current.querySelector('thead')?.contains(btn) || false,
};
break;
}
current = current.parentElement;
if (!current) break;
}
// Check parent context
let parent = btn.parentElement;
let parentContext = '';
for (let i = 0; i < 3; i++) {
if (parent) {
const parentText = parent.textContent?.trim() || '';
if (parentText.includes('Route') || parentText.includes('Static')) {
parentContext = 'ROUTE_CONTEXT';
break;
}
parent = parent.parentElement;
}
}
const buttonInfo = {
index,
text,
className: className.substring(0, 100),
id,
ariaLabel,
position: { x: rect.x, y: rect.y, width: rect.width, height: rect.height },
iconOnly: !text && btn.querySelector('svg') !== null,
nearTable,
hasRouteContext: parentContext === 'ROUTE_CONTEXT',
};
result.buttons.push(buttonInfo);
// Identify potential Add buttons
if (buttonInfo.iconOnly ||
text.toLowerCase().includes('add') ||
text.toLowerCase().includes('create') ||
className.toLowerCase().includes('add') ||
ariaLabel.toLowerCase().includes('add')) {
result.potentialAddButtons.push(buttonInfo);
}
}
}
});
return result;
});
console.log('📊 Page Analysis Results:');
console.log('='.repeat(80));
console.log(`\n📋 Tables Found: ${analysis.tables.length}`);
analysis.tables.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: ${table.buttonCount}`);
if (table.buttons.length > 0) {
console.log(` Button Details:`);
table.buttons.forEach((btn, j) => {
console.log(` ${j + 1}. "${btn.text}" - ${btn.className.substring(0, 60)}`);
console.log(` In Header: ${btn.isInHeader}, In Toolbar: ${btn.isInToolbar}`);
console.log(` Position: (${btn.position.x}, ${btn.position.y})`);
});
}
});
console.log(`\n🔘 All Buttons: ${analysis.buttons.length}`);
analysis.buttons.forEach((btn, i) => {
console.log(`\n${i + 1}. Button ${btn.index}:`);
console.log(` Text: "${btn.text}"`);
console.log(` Class: ${btn.className}`);
console.log(` Icon Only: ${btn.iconOnly}`);
console.log(` Near Table: ${btn.nearTable ? `Table ${btn.nearTable.index}` : 'No'}`);
console.log(` Has Route Context: ${btn.hasRouteContext}`);
console.log(` Position: (${btn.position.x}, ${btn.position.y})`);
});
console.log(`\n🎯 Potential Add Buttons: ${analysis.potentialAddButtons.length}`);
analysis.potentialAddButtons.forEach((btn, i) => {
console.log(`\n${i + 1}. "${btn.text}" - ${btn.className}`);
console.log(` Near Table: ${btn.nearTable ? `Table ${btn.nearTable.index}` : 'No'}`);
console.log(` Has Route Context: ${btn.hasRouteContext}`);
console.log(` Selector: ${btn.id ? `#${btn.id}` : `.${btn.className.split(' ')[0]}`}`);
});
// Test clicking potential buttons
console.log(`\n🧪 Testing Potential Add Buttons:`);
console.log('='.repeat(80));
for (const btn of analysis.potentialAddButtons.slice(0, 5)) {
try {
console.log(`\nTesting: "${btn.text}" (${btn.className.substring(0, 50)})`);
let selector = btn.id ? `#${btn.id}` : `button:has-text("${btn.text}")`;
if (!btn.text && btn.className) {
const firstClass = btn.className.split(' ')[0];
selector = `button.${firstClass}`;
}
const button = await page.locator(selector).first();
if (await button.isVisible({ timeout: 2000 })) {
console.log(` ✅ Button is visible`);
// Try clicking
await button.click({ timeout: 5000 }).catch(async (error) => {
console.log(` ⚠️ Regular click failed: ${error.message}`);
// Try JavaScript click
await page.evaluate((id) => {
const el = document.getElementById(id);
if (el) el.click();
}, btn.id).catch(() => {});
});
await page.waitForTimeout(3000);
// Check if form appeared
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(` Selector: ${selector}`);
console.log(` ID: ${btn.id || 'none'}`);
console.log(` Class: ${btn.className}`);
break;
} else {
// Check if menu appeared
const hasMenu = await page.locator('[role="menu"], [role="listbox"]').first().isVisible({ timeout: 2000 }).catch(() => false);
if (hasMenu) {
console.log(` ⚠️ Menu appeared (not form) - this might be a settings button`);
// Close menu
await page.keyboard.press('Escape');
await page.waitForTimeout(1000);
} else {
console.log(` ❌ No form or menu appeared`);
}
}
} else {
console.log(` ❌ Button not visible`);
}
} catch (error) {
console.log(` ❌ Error testing button: ${error.message}`);
}
}
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: 'find-add-error.png', fullPage: true });
} finally {
await browser.close();
}
})();