Files
aiform_prod/docs/N8N_FLIGHTS_TO_BASE64.js
AI Assistant 2e45786e46 feat: Telegram Mini App integration and UX improvements
- Добавлена полная интеграция с Telegram Mini App (динамическая загрузка SDK)
- Отдельный компактный дизайн для Telegram Mini App
- Добавлен loader при инициализации (предотвращает мелькание SMS-авторизации)
- Улучшена навигация: кнопки "Назад" и "К списку заявок" теперь сохраняют авторизацию
- Telegram Mini App: кнопка "Выход" просто закрывает приложение
- Telegram Mini App: заявки "В работе" скрыты из списка
- Веб-версия: для заявок "В работе" добавлена кнопка "Просмотреть в Telegram" (ссылка на @klientprav_bot)
- Telegram Mini App: кнопки действий в черновиках расположены вертикально
- Веб-версия: убрано отображение номера телефона в приветствии
- Исправлена проблема с возвратом к списку черновиков (не требует повторной SMS-авторизации)
- Заблокировано удаление и редактирование заявок со статусом "В работе"
- Добавлена документация по Telegram Mini App интеграции
2026-01-29 16:12:48 +03:00

631 lines
26 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ============================================================================
// n8n Code Node: Обработка данных о рейсах → Base64 HTML
// ============================================================================
// Вход: [{ data: [{ body: { flights: [...] }}, { body: { data: [...] }}] }]
// Выход: base64 HTML
// ============================================================================
const inputItems = $input.all();
// ================== FALLBACK ==================
if (!inputItems || inputItems.length === 0) {
const html = '<!DOCTYPE html><html><body><h1>Ошибка: данные не получены</h1></body></html>';
const htmlBase64 = Buffer.from(html, 'utf8').toString('base64');
return [{
json: {
html_base64: htmlBase64,
html: html,
flights_count: 0,
error: 'Нет входных данных'
}
}];
}
// ================== ИЗВЛЕЧЕНИЕ ДАННЫХ ==================
// Новая структура: [{ data: [{ body: { flights: [...] }}, { error: {...} }, { flight_number, ... }] }]
let flightAwareData = [];
let flightRadar24Data = [];
let requestData = null; // Данные из ноды "запрос рейса"
let flightRadar24Error = null; // Ошибка от FlightRadar24
try {
const firstItem = inputItems[0];
if (firstItem && firstItem.json && firstItem.json.data && Array.isArray(firstItem.json.data)) {
// Первый элемент массива data - FlightAware
if (firstItem.json.data[0] && firstItem.json.data[0].body) {
if (firstItem.json.data[0].body.flights) {
flightAwareData = Array.isArray(firstItem.json.data[0].body.flights)
? firstItem.json.data[0].body.flights
: [];
}
}
// Второй элемент массива data - FlightRadar24 (может быть ошибка)
if (firstItem.json.data[1]) {
// Проверяем, есть ли ошибка
if (firstItem.json.data[1].error) {
flightRadar24Error = firstItem.json.data[1].error;
console.log('⚠️ Ошибка FlightRadar24:', flightRadar24Error.message);
flightRadar24Data = [];
} else if (firstItem.json.data[1].body && firstItem.json.data[1].body.data) {
flightRadar24Data = Array.isArray(firstItem.json.data[1].body.data)
? firstItem.json.data[1].body.data
: [];
}
}
// Третий элемент массива data - данные из ноды "запрос рейса"
if (firstItem.json.data[2] && firstItem.json.data[2].flight_number) {
requestData = {
flight_number: firstItem.json.data[2].flight_number,
departure_date_local: firstItem.json.data[2].departure_date_local || null,
arrival_date_local: firstItem.json.data[2].arrival_date_local || null
};
console.log('✅ Данные запроса получены:', requestData);
}
}
} catch (e) {
console.log('⚠️ Ошибка извлечения данных:', e.message);
}
// ================== УТИЛИТЫ ==================
const safeStr = v => (v == null ? '' : String(v));
const safeDate = v => {
if (!v) return '—';
try {
const d = new Date(v);
return isNaN(d.getTime()) ? '—' : d.toLocaleString('ru-RU', {
timeZone: 'UTC',
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
});
} catch {
return '—';
}
};
const formatDuration = s => !s ? '—' : `${Math.floor(s / 3600)}ч ${Math.floor((s % 3600) / 60)}м`;
const formatDistance = km => !km ? '—' : `${Number(km).toFixed(2)} км`;
// ================== MERGE ПО REGISTRATION ==================
const flightsMap = new Map();
// Добавляем данные из FlightAware
flightAwareData.forEach(f => {
const reg = safeStr(f.registration).trim();
if (!reg) return;
if (!flightsMap.has(reg)) {
flightsMap.set(reg, {
registration: reg,
flightNumber: safeStr(f.flight_number),
ident: safeStr(f.ident),
identIata: safeStr(f.ident_iata),
aircraftType: safeStr(f.aircraft_type),
fa: f,
fr: null
});
} else {
flightsMap.get(reg).fa = f;
}
});
// Добавляем данные из FlightRadar24
flightRadar24Data.forEach(f => {
const reg = safeStr(f.reg).trim();
if (!reg) return;
if (!flightsMap.has(reg)) {
flightsMap.set(reg, {
registration: reg,
flightNumber: safeStr(f.flight),
ident: safeStr(f.callsign),
identIata: safeStr(f.flight),
aircraftType: safeStr(f.type),
fa: null,
fr: f
});
} else {
flightsMap.get(reg).fr = f;
}
});
// ================== ДОБАВЛЕНИЕ ЗАПРОШЕННЫХ РЕЙСОВ БЕЗ ДАННЫХ ==================
// Если есть информация о запрошенных рейсах, но нет данных - добавляем их
// Пытаемся извлечь из предыдущих нод (HTTP Request) или получить из входных данных
const allInputItems = $input.all();
const firstItemForRequest = inputItems[0]; // Используем уже определённую переменную из блока выше
// Ищем информацию о запрошенных рейсах
let requestedFlightNumbers = new Set();
// ВАРИАНТ 1: Получение данных из ноды "запрос рейса"
// Используем данные, извлечённые выше из data[2]
let requestFlightNumber = null;
let requestDepartureDate = null;
let requestArrivalDate = null;
if (requestData) {
requestFlightNumber = requestData.flight_number;
requestDepartureDate = requestData.departure_date_local;
requestArrivalDate = requestData.arrival_date_local;
if (requestFlightNumber) {
requestedFlightNumbers.add(String(requestFlightNumber));
}
}
// Дополнительно ищем в других местах (fallback)
allInputItems.forEach(item => {
if (item.json) {
// Прямые поля из ноды "запрос рейса"
if (item.json.flight_number && (item.json.departure_date_local || item.json.arrival_date_local)) {
if (!requestFlightNumber) {
requestFlightNumber = item.json.flight_number || item.json.ident || item.json.flight;
requestDepartureDate = item.json.departure_date_local || null;
requestArrivalDate = item.json.arrival_date_local || null;
if (requestFlightNumber) {
requestedFlightNumbers.add(String(requestFlightNumber));
}
}
}
// Данные, переданные из предыдущей ноды
if (item.json.request_flight_number) {
if (!requestFlightNumber) {
requestFlightNumber = item.json.request_flight_number;
requestDepartureDate = item.json.request_departure_date || null;
requestArrivalDate = item.json.request_arrival_date || null;
if (requestFlightNumber) {
requestedFlightNumbers.add(String(requestFlightNumber));
}
}
}
}
});
// ВАРИАНТ 2: Прямая передача из предыдущей ноды
if (firstItemForRequest && firstItemForRequest.json) {
// Массив запрошенных рейсов
if (firstItemForRequest.json.requested_flights && Array.isArray(firstItemForRequest.json.requested_flights)) {
firstItemForRequest.json.requested_flights.forEach(flight => {
const flightNum = typeof flight === 'string' ? flight : (flight.flight_number || flight.ident || flight);
if (flightNum) {
requestedFlightNumbers.add(flightNum);
}
});
}
// Один рейс
if (firstItemForRequest.json.flight_number || firstItemForRequest.json.ident || firstItemForRequest.json.flight) {
const flightNum = firstItemForRequest.json.flight_number || firstItemForRequest.json.ident || firstItemForRequest.json.flight;
requestedFlightNumbers.add(flightNum);
}
// Массив flight_numbers
if (firstItemForRequest.json.flight_numbers && Array.isArray(firstItemForRequest.json.flight_numbers)) {
firstItemForRequest.json.flight_numbers.forEach(flightNum => {
if (flightNum) requestedFlightNumbers.add(String(flightNum));
});
}
}
// ВАРИАНТ 3: Извлечение из всех входных элементов
allInputItems.forEach(item => {
if (item.json) {
if (item.json.flight_number) {
requestedFlightNumbers.add(String(item.json.flight_number));
}
if (item.json.ident) {
requestedFlightNumbers.add(String(item.json.ident));
}
if (item.json.flight) {
requestedFlightNumbers.add(String(item.json.flight));
}
}
});
// ВАРИАНТ 2: Извлечение из URL и параметров запросов
allInputItems.forEach(item => {
// Из URL запроса
if (item.json && item.json.url) {
const url = item.json.url;
const flightMatch = url.match(/(?:ident|flight_number|flight|callsign)=([^&]+)/i);
if (flightMatch) {
requestedFlightNumbers.add(flightMatch[1]);
}
}
// Из query параметров
if (item.json && item.json.query) {
const query = item.json.query;
const flightNum = query.ident || query.flight_number || query.flight || query.callsign;
if (flightNum) {
requestedFlightNumbers.add(flightNum);
}
}
// Из body запроса
if (item.json && item.json.body) {
const body = item.json.body;
const flightNum = body.ident || body.flight_number || body.flight || body.callsign;
if (flightNum) {
requestedFlightNumbers.add(flightNum);
}
}
// Прямо из json
if (item.json) {
const flightNum = item.json.ident || item.json.flight_number || item.json.flight || item.json.callsign;
if (flightNum) {
requestedFlightNumbers.add(flightNum);
}
}
});
// Добавляем запрошенные рейсы, для которых нет данных
requestedFlightNumbers.forEach(flightNum => {
// Проверяем, есть ли уже этот рейс в flightsMap
let found = false;
flightsMap.forEach((flight, reg) => {
if (flight.flightNumber === flightNum || flight.ident === flightNum || flight.identIata === flightNum) {
found = true;
}
});
// Если не найден - добавляем как запрошенный без данных
if (!found) {
flightsMap.set(`REQUESTED-${flightNum}`, {
registration: '—',
flightNumber: flightNum,
ident: flightNum,
identIata: flightNum,
aircraftType: '—',
fa: null,
fr: null,
isRequested: true // Флаг, что это запрошенный рейс без данных
});
}
});
const flights = Array.from(flightsMap.values());
// ================== HTML GENERATION ==================
// Делаем flightRadar24Error доступным в функции generateFlightCard
const generateFlightCard = (f, fr24ErrorParam = null) => {
const fa = f.fa;
const fr = f.fr;
const fr24Error = fr24ErrorParam; // Локальная переменная для использования в функции
// Если это запрошенный рейс без данных
if (f.isRequested && !fa && !fr) {
// Используем данные, полученные ранее из ноды "запрос рейса"
let requestInfo = '';
// Проверяем, соответствует ли этот рейс запрошенному
const matchesRequest = requestFlightNumber && (
String(f.flightNumber) === String(requestFlightNumber) ||
String(f.ident) === String(requestFlightNumber)
);
if (matchesRequest) {
if (requestDepartureDate) {
requestInfo += `<div class="info-row"><span class="label">Дата вылета (запрос):</span><span class="value">${requestDepartureDate}</span></div>`;
}
if (requestArrivalDate) {
requestInfo += `<div class="info-row"><span class="label">Дата прилёта (запрос):</span><span class="value">${requestArrivalDate}</span></div>`;
}
}
return `
<div class="flight-card">
<div class="flight-header">
<h2>Рейс ${f.flightNumber || f.ident || '—'}</h2>
<span class="registration">Запрошен</span>
</div>
<div class="flight-info">
<div class="info-row">
<span class="label">Запрошенный рейс:</span>
<span class="value">${f.flightNumber || f.ident || '—'}</span>
</div>
${requestInfo}
</div>
<div class="source-section">
<div class="source-header">
<span class="source-badge source-flightaware">FlightAware</span>
<span class="source-missing">Данные не получены</span>
</div>
<div class="source-content">
<div style="padding: 10px; color: #666; font-size: 13px;">
По запросу рейса <strong>${f.flightNumber || f.ident || '—'}</strong>${requestDepartureDate ? ` на ${requestDepartureDate}` : ''} данные не найдены.
</div>
</div>
</div>
<div class="source-section">
<div class="source-header">
<span class="source-badge source-flightradar24">FlightRadar24</span>
<span class="source-missing">Данные не получены</span>
</div>
<div class="source-content">
<div style="padding: 10px; color: #666; font-size: 13px;">
По запросу рейса <strong>${f.flightNumber || f.ident || '—'}</strong>${requestDepartureDate ? ` на ${requestDepartureDate}` : ''} данные не найдены.
</div>
</div>
</div>
</div>`;
}
let card = `
<div class="flight-card">
<div class="flight-header">
<h2>Рейс ${f.flightNumber || f.ident || '—'}</h2>
<span class="registration">${f.registration || '—'}</span>
</div>
<div class="flight-info">
<div class="info-row">
<span class="label">Тип самолёта:</span>
<span class="value">${f.aircraftType || '—'}</span>
</div>
<div class="info-row">
<span class="label">Идентификатор:</span>
<span class="value">${f.ident || '—'} (${f.identIata || '—'})</span>
</div>
</div>`;
// Данные из FlightAware
if (fa) {
card += `
<div class="source-section">
<div class="source-header">
<span class="source-badge source-flightaware">FlightAware</span>
</div>
<div class="source-content">
<div class="route-info">
<div class="route-item">
<span class="route-label">Откуда:</span>
<span class="route-value">${safeStr(fa.origin?.name || fa.origin?.code_iata || '—')} (${safeStr(fa.origin?.code_iata || '—')})</span>
</div>
<div class="route-item">
<span class="route-label">Куда:</span>
<span class="route-value">${safeStr(fa.destination?.name || fa.destination?.code_iata || '—')} (${safeStr(fa.destination?.code_iata || '—')})</span>
</div>
</div>
<div class="timeline">
<div class="timeline-item">
<span class="timeline-label">Плановый вылет:</span>
<span class="timeline-value">${safeDate(fa.scheduled_out)}</span>
</div>
<div class="timeline-item">
<span class="timeline-label">Фактический вылет:</span>
<span class="timeline-value">${safeDate(fa.actual_out)}</span>
</div>
<div class="timeline-item">
<span class="timeline-label">Взлёт:</span>
<span class="timeline-value">${safeDate(fa.actual_off)} ${fa.actual_runway_off ? `(ВПП ${fa.actual_runway_off})` : ''}</span>
</div>
<div class="timeline-item">
<span class="timeline-label">Посадка:</span>
<span class="timeline-value">${safeDate(fa.actual_on)} ${fa.actual_runway_on ? `(ВПП ${fa.actual_runway_on})` : ''}</span>
</div>
<div class="timeline-item">
<span class="timeline-label">Фактический прилёт:</span>
<span class="timeline-value">${safeDate(fa.actual_in)}</span>
</div>
</div>
<div class="status-info">
<div class="status-item">
<span class="status-label">Статус:</span>
<span class="status-value">${safeStr(fa.status || '—')}</span>
</div>
${fa.departure_delay !== null && fa.departure_delay !== undefined ? `
<div class="status-item">
<span class="status-label">Задержка вылета:</span>
<span class="status-value ${fa.departure_delay < 0 ? 'delay-negative' : 'delay-positive'}">${fa.departure_delay > 0 ? '+' : ''}${Math.floor(fa.departure_delay / 60)} мин</span>
</div>
` : ''}
${fa.arrival_delay !== null && fa.arrival_delay !== undefined ? `
<div class="status-item">
<span class="status-label">Задержка прилёта:</span>
<span class="status-value ${fa.arrival_delay < 0 ? 'delay-negative' : 'delay-positive'}">${fa.arrival_delay > 0 ? '+' : ''}${Math.floor(fa.arrival_delay / 60)} мин</span>
</div>
` : ''}
${fa.gate_origin ? `
<div class="status-item">
<span class="status-label">Гейт вылета:</span>
<span class="status-value">${fa.gate_origin}</span>
</div>
` : ''}
${fa.gate_destination ? `
<div class="status-item">
<span class="status-label">Гейт прилёта:</span>
<span class="status-value">${fa.gate_destination}</span>
</div>
` : ''}
${fa.baggage_claim ? `
<div class="status-item">
<span class="status-label">Выдача багажа:</span>
<span class="status-value">${fa.baggage_claim}</span>
</div>
` : ''}
</div>
</div>
</div>`;
} else {
card += `
<div class="source-section">
<div class="source-header">
<span class="source-badge source-flightaware">FlightAware</span>
<span class="source-missing">Данные не получены</span>
</div>
</div>`;
}
// Данные из FlightRadar24
if (fr) {
card += `
<div class="source-section">
<div class="source-header">
<span class="source-badge source-flightradar24">FlightRadar24</span>
</div>
<div class="source-content">
<div class="route-info">
<div class="route-item">
<span class="route-label">Откуда:</span>
<span class="route-value">${safeStr(fr.orig_iata || '—')} (${safeStr(fr.orig_icao || '—')})</span>
</div>
<div class="route-item">
<span class="route-label">Куда:</span>
<span class="route-value">${safeStr(fr.dest_iata || '—')} (${safeStr(fr.dest_icao || '—')})</span>
</div>
</div>
<div class="timeline">
<div class="timeline-item">
<span class="timeline-label">Взлёт:</span>
<span class="timeline-value">${safeDate(fr.datetime_takeoff)} ${fr.runway_takeoff ? `(ВПП ${fr.runway_takeoff})` : ''}</span>
</div>
<div class="timeline-item">
<span class="timeline-label">Посадка:</span>
<span class="timeline-value">${safeDate(fr.datetime_landed)} ${fr.runway_landed ? `(ВПП ${fr.runway_landed})` : ''}</span>
</div>
</div>
<div class="status-info">
<div class="status-item">
<span class="status-label">Время полёта:</span>
<span class="status-value">${formatDuration(fr.flight_time)}</span>
</div>
<div class="status-item">
<span class="status-label">Фактическое расстояние:</span>
<span class="status-value">${formatDistance(fr.actual_distance)}</span>
</div>
<div class="status-item">
<span class="status-label">Кратчайшее расстояние:</span>
<span class="status-value">${formatDistance(fr.circle_distance)}</span>
</div>
<div class="status-item">
<span class="status-label">Статус полёта:</span>
<span class="status-value">${fr.flight_ended ? 'Завершён' : 'В процессе'}</span>
</div>
</div>
</div>
</div>`;
} else {
card += `
<div class="source-section">
<div class="source-header">
<span class="source-badge source-flightradar24">FlightRadar24</span>
<span class="source-missing">Данные не получены</span>
</div>
</div>`;
}
card += `</div>`;
return card;
};
// Генерация полного HTML
const now = new Date();
const reportDate = now.toLocaleString('ru-RU', {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
const html = `<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Отчёт о рейсах</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; line-height: 1.4; color: #333; background: #f5f5f5; padding: 15px; }
.container { max-width: 1200px; margin: 0 auto; background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
.header { border-bottom: 3px solid #2563eb; padding-bottom: 8px; margin-bottom: 8px; }
.header h1 { color: #1e40af; font-size: 24px; margin-bottom: 4px; }
.header-meta { color: #666; font-size: 13px; }
.sources-info { display: flex; gap: 10px; margin-top: 4px; flex-wrap: wrap; }
.source-tag { display: inline-block; padding: 3px 10px; border-radius: 12px; font-size: 11px; font-weight: 500; }
.source-tag.available { background: #d1fae5; color: #065f46; }
.source-tag.unavailable { background: #fee2e2; color: #991b1b; }
.flight-card { border: 1px solid #e5e7eb; border-radius: 8px; margin-bottom: 18px; overflow: hidden; background: white; }
.flight-header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 14px 18px; display: flex; justify-content: space-between; align-items: center; }
.flight-header h2 { font-size: 20px; margin: 0; }
.registration { background: rgba(255,255,255,0.2); padding: 4px 10px; border-radius: 4px; font-weight: 600; font-size: 13px; }
.flight-info { padding: 12px 18px; background: #f9fafb; border-bottom: 1px solid #e5e7eb; }
.info-row { display: flex; margin-bottom: 6px; }
.info-row:last-child { margin-bottom: 0; }
.info-row .label { font-weight: 600; color: #4b5563; width: 140px; flex-shrink: 0; font-size: 13px; }
.info-row .value { color: #111827; font-size: 13px; }
.source-section { border-top: 1px solid #e5e7eb; padding: 12px 18px; }
.source-section:first-of-type { border-top: none; }
.source-header { display: flex; align-items: center; gap: 8px; margin-bottom: 10px; }
.source-badge { display: inline-block; padding: 5px 12px; border-radius: 5px; font-size: 12px; font-weight: 600; color: white; }
.source-badge.source-flightaware { background: #3b82f6; }
.source-badge.source-flightradar24 { background: #10b981; }
.source-missing { color: #ef4444; font-size: 12px; font-style: italic; }
.source-content { margin-left: 0; }
.route-info { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-bottom: 12px; padding: 12px; background: #f9fafb; border-radius: 6px; }
.route-item { display: flex; flex-direction: column; }
.route-label { font-size: 11px; color: #6b7280; margin-bottom: 3px; text-transform: uppercase; letter-spacing: 0.5px; }
.route-value { font-size: 14px; font-weight: 600; color: #111827; }
.timeline { margin-bottom: 12px; }
.timeline-item { display: flex; justify-content: space-between; padding: 6px 0; border-bottom: 1px solid #e5e7eb; }
.timeline-item:last-child { border-bottom: none; }
.timeline-label { font-weight: 500; color: #4b5563; width: 160px; flex-shrink: 0; font-size: 12px; }
.timeline-value { color: #111827; text-align: right; font-size: 12px; }
.status-info { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 10px; padding: 12px; background: #f9fafb; border-radius: 6px; }
.status-item { display: flex; flex-direction: column; }
.status-label { font-size: 11px; color: #6b7280; margin-bottom: 3px; text-transform: uppercase; letter-spacing: 0.5px; }
.status-value { font-size: 13px; font-weight: 600; color: #111827; }
.delay-negative { color: #10b981; }
.delay-positive { color: #ef4444; }
.no-data { text-align: center; padding: 40px 20px; color: #6b7280; font-size: 16px; }
@media print { body { background: white; padding: 0; } .container { box-shadow: none; padding: 15px; } .flight-card { page-break-inside: avoid; margin-bottom: 15px; } }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>Отчёт о рейсах</h1>
<div class="header-meta">
<div>Дата формирования: ${reportDate}</div>
<div class="sources-info">
<span class="source-tag ${flightAwareData.length > 0 ? 'available' : 'unavailable'}">
FlightAware: ${flightAwareData.length > 0 ? '✓ Данные получены' : '✗ Данные отсутствуют'}
</span>
<span class="source-tag ${flightRadar24Data.length > 0 ? 'available' : 'unavailable'}">
FlightRadar24: ${flightRadar24Data.length > 0 ? '✓ Данные получены' : '✗ Данные отсутствуют'}
</span>
</div>
</div>
</div>
<div class="flights-container">
${flights.length ? flights.map(f => generateFlightCard(f, flightRadar24Error)).join('') : '<div class="no-data">Данные о рейсах не найдены</div>'}
</div>
</div>
</body>
</html>`;
// ================== HTML → BASE64 ==================
const htmlBase64 = Buffer.from(html, 'utf8').toString('base64');
// ================== RETURN ==================
return [{
json: {
html_base64: htmlBase64,
html: html,
flights_count: flights.length,
sources: {
flightaware: { available: flightAwareData.length > 0, count: flightAwareData.length },
flightradar24: { available: flightRadar24Data.length > 0, count: flightRadar24Data.length }
},
generated_at: now.toISOString()
}
}];