No services configured. Run the installer to set up services.
`;
return;
}
// Sort all services alphabetically by display name
const sortedKeys = Object.keys(services).sort((a, b) => {
const aName = (SERVICE_METADATA[a]?.name || a).toLowerCase();
const bName = (SERVICE_METADATA[b]?.name || b).toLowerCase();
return aName.localeCompare(bName);
});
// Use DocumentFragment for better performance
const fragment = document.createDocumentFragment();
sortedKeys.forEach(key => {
fragment.appendChild(renderServiceCard(key, services[key]));
});
servicesContainer.appendChild(fragment);
}
/**
* Render quick start steps
*/
function renderQuickStart(steps) {
if (!quickstartContainer) return;
quickstartContainer.innerHTML = '';
if (!steps || steps.length === 0) {
// Default steps if none provided
steps = [
{ step: 1, title: 'Log into n8n', description: 'Use the email you provided during installation' },
{ step: 2, title: 'Create your first workflow', description: 'Start with a Manual Trigger + HTTP Request nodes' },
{ step: 3, title: 'Monitor your system', description: 'Use Grafana to track performance' }
];
}
const fragment = document.createDocumentFragment();
steps.forEach(item => {
const stepEl = document.createElement('div');
stepEl.className = 'flex items-start gap-4 p-4 bg-surface-100 rounded-xl border border-surface-400 hover:border-brand/30 hover:bg-surface-200 transition-all';
stepEl.innerHTML = `
${Icons.warning('w-12 h-12 mx-auto text-red-500 mb-4')}
Unable to load service data
Make sure the installation completed successfully and data.json was generated.
`;
}
/**
* Load data and render page
*/
async function init() {
// Inject section icons
injectSectionIcons();
// Always render commands (static content)
renderCommands();
// Fetch both JSON files in parallel for better performance
// Each fetch is handled independently - changelog failure won't affect main data
const [changelogResult, dataResult] = await Promise.allSettled([
fetch('changelog.json').then(r => r.ok ? r.json() : null),
fetch('data.json').then(r => r.ok ? r.json() : Promise.reject(new Error(`HTTP ${r.status}`)))
]);
// Handle changelog (independent - failures don't break the page)
if (changelogResult.status === 'fulfilled' && changelogResult.value?.content) {
renderChangelog(changelogResult.value.content);
} else {
if (changelogResult.status === 'rejected') {
console.error('Error loading changelog:', changelogResult.reason);
}
renderChangelog(null);
}
// Handle main data
if (dataResult.status === 'fulfilled' && dataResult.value) {
const data = dataResult.value;
// Update domain info
if (domainInfo) {
if (data.domain) {
domainInfo.textContent = `Domain: ${data.domain}`;
}
if (data.generated_at) {
const date = new Date(data.generated_at);
domainInfo.textContent += ` | Generated: ${date.toLocaleString()}`;
}
}
// Render services
renderServices(data.services);
// Render quick start
renderQuickStart(data.quick_start);
} else {
console.error('Error loading data:', dataResult.reason);
// Show error in UI
renderServicesError();
// Still render default quick start
renderQuickStart(null);
}
// Initialize confetti animation on first visit
CinematicAnimations.init();
}
// Initialize when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();