Honest pricing review with disclaimers

What Changed:
- Added VERIFIED-PRICES.md with honest assessment
- Added BUDGET-REALITY.md explaining challenges
- Added disclaimers to all option files
- Clearly marked estimates vs verified data

Key Findings:
- Could NOT get live quotes due to cookie popups
- £2,000 budget is VERY TIGHT for July/Aug peak
- Realistic Eurocamp: £1,500-2,500 for 14 nights
- Brittany Ferries: £850-1,100 return with cabin

Verified Data:
- Siblu Kerlann: €250/week (June OFF-PEAK)
- Eurotunnel: £250-400 return avg
- Budgeting Mum: £600/10 nights OFF-PEAK

User action needed:
- Manually check Eurocamp.co.uk
- Consider shorter duration
- Consider gîte instead of mobile home
This commit is contained in:
2026-03-15 23:18:43 +00:00
commit a27fcfef61
640 changed files with 179624 additions and 0 deletions

337
brittany-search-v2.js Normal file
View File

@@ -0,0 +1,337 @@
const { chromium } = require('playwright');
const fs = require('fs');
const path = require('path');
const SCREENSHOT_DIR = path.join(process.env.HOME, 'holiday-planning', 'price-evidence');
const OUTPUT_DIR = path.join(process.env.HOME, 'holiday-planning', 'prices');
async function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function safeClick(page, selector, description) {
try {
const el = page.locator(selector).first();
if (await el.isVisible({ timeout: 3000 })) {
await el.click();
console.log(`Clicked: ${description}`);
await sleep(500);
return true;
}
} catch (e) {
console.log(`Could not click ${description}: ${e.message}`);
}
return false;
}
async function main() {
console.log('Starting Brittany Ferries price search v2...');
console.log('Date: 18 July 2026 - 2 August 2026, Plymouth-Roscoff, 2 adults, 1 child (6), 1 car');
const browser = await chromium.launch({
headless: true,
args: ['--no-sandbox', '--disable-setuid-sandbox']
});
const context = await browser.newContext({
viewport: { width: 1920, height: 1080 },
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
});
const page = await context.newPage();
const results = {
searchDate: new Date().toISOString(),
route: 'Plymouth to Roscoff',
outboundDate: '2026-07-18',
returnDate: '2026-08-02',
passengers: { adults: 2, children: 1, childAges: [6] },
vehicle: '1 car',
screenshots: [],
status: 'in_progress',
steps: []
};
try {
// Step 1: Navigate to Brittany Ferries ferry booking page
console.log('\n=== Step 1: Navigate to booking page ===');
await page.goto('https://www.brittany-ferries.co.uk/ferry', { waitUntil: 'networkidle', timeout: 60000 });
await sleep(3000);
await page.screenshot({ path: path.join(SCREENSHOT_DIR, 'v2-01-ferry-page.png'), fullPage: false });
results.screenshots.push('v2-01-ferry-page.png');
results.steps.push('Loaded ferry booking page');
// Accept cookies
console.log('\n=== Step 2: Accept cookies ===');
try {
const acceptBtn = page.locator('#onetrust-accept-btn-handler').first();
if (await acceptBtn.isVisible({ timeout: 3000 })) {
await acceptBtn.click();
console.log('Accepted cookies');
await sleep(1000);
results.steps.push('Accepted cookies');
}
} catch (e) {
console.log('No cookie banner visible');
}
await page.screenshot({ path: path.join(SCREENSHOT_DIR, 'v2-02-after-cookies.png'), fullPage: false });
results.screenshots.push('v2-02-after-cookies.png');
// Step 3: Select route - Plymouth to Roscoff
console.log('\n=== Step 3: Select route ===');
// Look for route selection dropdowns - they might be custom Angular components
const routeSelectors = await page.locator('[class*="route"], [class*="departure"], [class*="arrival"]').all();
console.log(`Found ${routeSelectors.length} route-related elements`);
// Try to find and click the departure port dropdown
const departureSelectors = [
'mat-form-field:has-text("From")',
'mat-form-field:has-text("Departure")',
'[data-cy="departure-port"]',
'.departure-port-selector',
'mat-select:has-text("Select")'
];
let departureClicked = false;
for (const selector of departureSelectors) {
try {
const el = page.locator(selector).first();
if (await el.isVisible({ timeout: 2000 })) {
await el.click();
console.log(`Clicked departure selector: ${selector}`);
departureClicked = true;
await sleep(1000);
break;
}
} catch (e) {}
}
// Try clicking on mat-select elements
if (!departureClicked) {
const matSelects = await page.locator('mat-select').all();
console.log(`Found ${matSelects.length} mat-select elements`);
// First mat-select is likely departure port
if (matSelects.length > 0) {
await matSelects[0].click();
console.log('Clicked first mat-select (departure port)');
await sleep(1000);
await page.screenshot({ path: path.join(SCREENSHOT_DIR, 'v2-03-departure-dropdown.png'), fullPage: false });
results.screenshots.push('v2-03-departure-dropdown.png');
// Look for Plymouth option
const plymouthOptions = [
'mat-option:has-text("Plymouth")',
'.mat-option:has-text("Plymouth")',
'span:has-text("Plymouth")'
];
for (const opt of plymouthOptions) {
try {
const optEl = page.locator(opt).first();
if (await optEl.isVisible({ timeout: 2000 })) {
await optEl.click();
console.log('Selected Plymouth');
results.steps.push('Selected Plymouth as departure');
break;
}
} catch (e) {}
}
await sleep(500);
}
}
await page.screenshot({ path: path.join(SCREENSHOT_DIR, 'v2-04-after-plymouth.png'), fullPage: false });
results.screenshots.push('v2-04-after-plymouth.png');
// Select arrival port - Roscoff
const matSelects2 = await page.locator('mat-select').all();
if (matSelects2.length > 1) {
await matSelects2[1].click();
console.log('Clicked second mat-select (arrival port)');
await sleep(1000);
await page.screenshot({ path: path.join(SCREENSHOT_DIR, 'v2-05-arrival-dropdown.png'), fullPage: false });
results.screenshots.push('v2-05-arrival-dropdown.png');
// Look for Roscoff option
const roscoffOptions = [
'mat-option:has-text("Roscoff")',
'.mat-option:has-text("Roscoff")',
'span:has-text("Roscoff")'
];
for (const opt of roscoffOptions) {
try {
const optEl = page.locator(opt).first();
if (await optEl.isVisible({ timeout: 2000 })) {
await optEl.click();
console.log('Selected Roscoff');
results.steps.push('Selected Roscoff as arrival');
break;
}
} catch (e) {}
}
await sleep(500);
}
await page.screenshot({ path: path.join(SCREENSHOT_DIR, 'v2-06-after-roscoff.png'), fullPage: false });
results.screenshots.push('v2-06-after-roscoff.png');
// Step 4: Enter dates
console.log('\n=== Step 4: Enter dates ===');
// Look for date inputs
const dateInputs = await page.locator('input[placeholder*="date"], input[type="text"]').all();
console.log(`Found ${dateInputs.length} text inputs`);
// Find outbound date input
const outboundInput = page.locator('input[placeholder*="Outbound"]').first();
if (await outboundInput.isVisible({ timeout: 2000 })) {
await outboundInput.click();
await sleep(500);
await outboundInput.fill('18/07/2026');
console.log('Entered outbound date: 18/07/2026');
await page.keyboard.press('Enter');
await sleep(500);
results.steps.push('Entered outbound date: 18/07/2026');
}
// Find inbound date input
const inboundInput = page.locator('input[placeholder*="Inbound"], input[placeholder*="Return"]').first();
if (await inboundInput.isVisible({ timeout: 2000 })) {
await inboundInput.click();
await sleep(500);
await inboundInput.fill('02/08/2026');
console.log('Entered inbound date: 02/08/2026');
await page.keyboard.press('Enter');
await sleep(500);
results.steps.push('Entered inbound date: 02/08/2026');
}
await page.screenshot({ path: path.join(SCREENSHOT_DIR, 'v2-07-dates-entered.png'), fullPage: false });
results.screenshots.push('v2-07-dates-entered.png');
// Step 5: Configure passengers and vehicle
console.log('\n=== Step 5: Configure passengers and vehicle ===');
// Look for passenger selector
const passengerSelectors = await page.locator('[class*="passenger"], mat-select, [data-cy*="passenger"]').all();
console.log(`Found ${passengerSelectors.length} potential passenger selectors`);
// Try to find and interact with passenger dropdowns
// Usually there's a dropdown for adults, children, and vehicles
// Check if there are specific passenger count controls
const adultPlus = page.locator('[class*="adult"] button:has-text("+"), [data-cy="adult-plus"]').first();
const adultMinus = page.locator('[class*="adult"] button:has-text("-"), [data-cy="adult-minus"]').first();
// We need 2 adults - check current state and adjust
// This is tricky without seeing the actual UI
// Look for any dropdown that might be for passengers
const allSelects = await page.locator('mat-select').all();
console.log(`Total mat-select elements: ${allSelects.length}`);
await page.screenshot({ path: path.join(SCREENSHOT_DIR, 'v2-08-before-passengers.png'), fullPage: false });
results.screenshots.push('v2-08-before-passengers.png');
// Step 6: Submit search
console.log('\n=== Step 6: Submit search ===');
const searchButtons = [
'button:has-text("Search")',
'button:has-text("Get quotes")',
'button:has-text("Find ferries")',
'button[type="submit"]',
'.search-button',
'[data-cy="search-button"]'
];
for (const btn of searchButtons) {
try {
const searchBtn = page.locator(btn).first();
if (await searchBtn.isVisible({ timeout: 1000 })) {
await searchBtn.click();
console.log(`Clicked search button: ${btn}`);
results.steps.push('Clicked search button');
break;
}
} catch (e) {}
}
// Wait for results
console.log('Waiting for results...');
await sleep(5000);
await page.screenshot({ path: path.join(SCREENSHOT_DIR, 'v2-09-results-page.png'), fullPage: true });
results.screenshots.push('v2-09-results-page.png');
// Step 7: Extract pricing information
console.log('\n=== Step 7: Extract pricing ===');
const pageContent = await page.content();
fs.writeFileSync(path.join(SCREENSHOT_DIR, 'v2-results-page.html'), pageContent);
const pageText = await page.evaluate(() => document.body.innerText);
fs.writeFileSync(path.join(SCREENSHOT_DIR, 'v2-results-page.txt'), pageText);
// Try to extract price information
const pricePatterns = [
/£\d+/g,
/\d+\.\d{2}/g,
/total.*?£?(\d+)/gi
];
const prices = [];
for (const pattern of pricePatterns) {
const matches = pageText.match(pattern);
if (matches) {
prices.push(...matches);
}
}
console.log('Found potential prices:', prices.slice(0, 20));
results.extractedPrices = [...new Set(prices)].slice(0, 20);
// Try to find specific price elements
const priceElements = await page.locator('[class*="price"], [data-cy*="price"]').all();
const elementPrices = [];
for (const el of priceElements.slice(0, 10)) {
try {
const text = await el.textContent();
elementPrices.push(text);
} catch (e) {}
}
console.log('Price elements found:', elementPrices);
results.priceElements = elementPrices;
results.url = page.url();
results.status = 'completed';
results.finalUrl = page.url();
} catch (error) {
console.error('Error during scraping:', error);
results.status = 'error';
results.error = error.message;
results.stack = error.stack;
await page.screenshot({ path: path.join(SCREENSHOT_DIR, 'v2-error.png'), fullPage: true });
results.screenshots.push('v2-error.png');
}
await browser.close();
// Save results
const outputPath = path.join(OUTPUT_DIR, 'brittany-ferries-plymouth-roscoff.json');
fs.writeFileSync(outputPath, JSON.stringify(results, null, 2));
console.log(`\n=== Results saved to: ${outputPath} ===`);
return results;
}
main().catch(console.error);