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>
349 lines
13 KiB
JavaScript
Executable File
349 lines
13 KiB
JavaScript
Executable File
#!/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();
|
||
}
|
||
})();
|