/* Editor logic - initialConfig: provided default config (from your JSON) - render(): placeholder that generates HTML and writes to iframe.srcdoc - inputs call render() on change - "Open in new tab" opens the rendered HTML in a new window using a blob URL */ import ejs from 'ejs'; import templateContent from './template.ejs?raw'; import 'bootstrap/js/src/modal.js'; import Popover from 'bootstrap/js/src/popover.js'; import 'bootstrap/dist/css/bootstrap.min.css'; import { jsCodeGen, jsonCodeGen, pythonCodeGen } from './codegen'; let template = ejs.compile(templateContent); // can be changed if specified by '?from=' let initialConfig = { title: 'Internal server error', error_code: '500', more_information: { hidden: false, text: 'cloudflare.com', for: 'more information', }, browser_status: { status: 'ok', location: 'You', name: 'Browser', status_text: 'Working', }, cloudflare_status: { status: 'error', location: 'San Francisco', name: 'Cloudflare', status_text: 'Error', }, host_status: { status: 'ok', location: 'Website', name: 'Host', status_text: 'Working', }, error_source: 'cloudflare', what_happened: "There is an internal server error on Cloudflare's network.", what_can_i_do: 'Please try again in a few minutes.', }; // Demo presets (content brief — replace or expand as needed) const PRESETS = { default: structuredClone(initialConfig), empty: { error_code: '500', }, catastrophic: { title: 'Catastrophic infrastructure failure', error_code: '500', more_information: { for: 'no help at all', }, browser_status: { status: 'error', status_text: 'Out of Memory', }, cloudflare_status: { status: 'error', location: 'Everywhere', status_text: 'Error', }, host_status: { status: 'error', location: 'example.com', status_text: 'On Fire', }, error_source: 'cloudflare', what_happened: 'There is a catastrophic failure.', what_can_i_do: 'Please try again in a few years.', }, working: { title: 'Web server is working', error_code: '200', more_information: { hidden: true, }, browser_status: { status: 'ok', status_text: 'Seems Working', }, cloudflare_status: { status: 'ok', status_text: 'Often Working', }, host_status: { status: 'ok', location: 'example.com', status_text: 'Almost Working', }, error_source: 'host', what_happened: 'This site is still working. And it looks great.', what_can_i_do: 'Visit the site before it crashes someday.', }, consensual: { title: 'The Myth Of "Consensual" Internet', error_code: 'lmao', more_information: { hidden: false, text: 'r/ProgrammerHumor', link: 'https://redd.it/1p2yola', }, browser_status: { status: 'ok', location: 'You', name: 'Browser', status_text: 'I Consent', }, cloudflare_status: { status: 'error', location: 'F***ing Everywhere', name: 'Cloudflare', status_text: "I Don't!", }, host_status: { status: 'ok', location: 'Remote', name: 'Host', status_text: 'I Consent', }, error_source: 'cloudflare', what_happened: "Isn't There Someone You Forgot To Ask?", what_can_i_do: 'Kill Yourself', }, }; function extractUrlParam(str, key) { const urlParams = new URLSearchParams(str); return urlParams.get(key); } function getDefaultPresetName() { const key = 'from'; let name = extractUrlParam(window.location.search, key); if (!name) { name = extractUrlParam(window.location.hash.substring(1), key); } if (name) { name = name.replace(/[^\w\d]/g, ''); } return name; } const defaultPresetName = getDefaultPresetName(); if (defaultPresetName && defaultPresetName.indexOf('/') < 0) { fetch(`../s/${defaultPresetName}`, { headers: { Accept: 'application/json', }, }) .then((response) => { if (!response.ok) { throw new Error('failed to get preset'); } return response.json(); }) .then((result) => { if (result.status != 'ok') { return; } console.log(result.parameters); initialConfig = result.parameters; loadConfig(initialConfig); render(); }); } /* Utilities */ function $(id) { return document.getElementById(id); } /* Fill form from config */ function loadConfig(cfg) { $('title').value = cfg.title ?? ''; $('error_code').value = cfg.error_code ?? ''; $('more_hidden').checked = !!(cfg.more_information && cfg.more_information.hidden); $('more_text').value = cfg.more_information?.text ?? ''; $('more_link').value = cfg.more_information?.link ?? ''; $('more_for').value = cfg.more_information?.for ?? ''; $('browser_status').value = cfg.browser_status?.status ?? 'ok'; $('browser_location').value = cfg.browser_status?.location ?? ''; $('browser_name').value = cfg.browser_status?.name ?? ''; $('browser_status_text').value = cfg.browser_status?.status_text ?? ''; $('cloudflare_status').value = cfg.cloudflare_status?.status ?? 'ok'; $('cloudflare_location').value = cfg.cloudflare_status?.location ?? ''; $('cloudflare_name').value = cfg.cloudflare_status?.name ?? ''; $('cloudflare_status_text').value = cfg.cloudflare_status?.status_text ?? ''; $('host_status').value = cfg.host_status?.status ?? 'ok'; $('host_location').value = cfg.host_status?.location ?? ''; $('host_name').value = cfg.host_status?.name ?? ''; $('host_status_text').value = cfg.host_status?.status_text ?? ''; if (cfg.error_source === 'browser') $('err_browser').checked = true; else if (cfg.error_source === 'cloudflare') $('err_cloudflare').checked = true; else $('err_host').checked = true; $('what_happened').value = cfg.what_happened ?? ''; $('what_can_i_do').value = cfg.what_can_i_do ?? ''; $('perf_text').value = cfg.perf_sec_by?.text ?? ''; $('perf_link').value = cfg.perf_sec_by?.link ?? ''; } /* Read config from form inputs */ function readConfig() { return { title: $('title').value, error_code: $('error_code').value, more_information: { hidden: !!$('more_hidden').checked, text: $('more_text').value, link: $('more_link').value, for: $('more_for').value, }, browser_status: { status: $('browser_status').value, location: $('browser_location').value, name: $('browser_name').value, status_text: $('browser_status_text').value, }, cloudflare_status: { status: $('cloudflare_status').value, location: $('cloudflare_location').value, name: $('cloudflare_name').value, status_text: $('cloudflare_status_text').value, }, host_status: { status: $('host_status').value, location: $('host_location').value, name: $('host_name').value, status_text: $('host_status_text').value, }, error_source: ( document.querySelector('input[name="error_source"]:checked') || { value: 'host', } ).value, what_happened: $('what_happened').value, what_can_i_do: $('what_can_i_do').value, perf_sec_by: { text: $('perf_text').value, link: $('perf_link').value, }, }; } function formatUtcTimestamp() { const d = new Date(); const year = d.getUTCFullYear(); const month = String(d.getUTCMonth() + 1).padStart(2, '0'); const day = String(d.getUTCDate()).padStart(2, '0'); const hours = String(d.getUTCHours()).padStart(2, '0'); const minutes = String(d.getUTCMinutes()).padStart(2, '0'); const seconds = String(d.getUTCSeconds()).padStart(2, '0'); return `${year}-${month}-${day} ${hours}:${minutes}:${seconds} UTC`; } function renderEjs(params) { return template({ params: params, }); } /* Basic render: build HTML string from config and put into iframe.srcdoc */ function render() { const cfg = readConfig(); window.lastCfg = cfg; cfg.time = formatUtcTimestamp(); cfg.ray_id = '0123456789abcdef'; cfg.client_ip = '1.1.1.1'; if (Number.isNaN(Number(cfg.error_code))) { cfg.html_title = cfg.title || 'Internal server error'; } let pageHtml = renderEjs(cfg); // Write into iframe const iframe = $('previewFrame'); let doc = iframe.contentDocument; doc.open(); doc.write(pageHtml); doc.close(); updateStatusBlockStyles(); // store last rendered HTML for "open in new tab" lastRenderedHtml = pageHtml; } /* Open in new tab: create blob and open */ let lastRenderedHtml = ''; function openInNewTab() { if (!lastRenderedHtml) render(); const wnd = window.open() wnd.document.documentElement.innerHTML = lastRenderedHtml } function createShareableLink() { $('shareLink').value = ''; fetch('../s/create', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ parameters: window.lastCfg, }), }) .then((response) => { if (!response.ok) { alert('failed to create link'); } return response.json(); }) .then((result) => { if (result.status != 'ok') { alert('failed to create link'); return; } $('shareLink').value = result.url; }); } function resizePreviewFrame() { const iframe = $('previewFrame'); const height = iframe.contentWindow.document.body.scrollHeight + 2; iframe.style.setProperty('--expanded-height', height + 'px'); } /* Update status block colors based on selected status and error_source */ function updateStatusBlockStyles() { const browserOk = $('browser_status').value === 'ok'; const cfOk = $('cloudflare_status').value === 'ok'; const hostOk = $('host_status').value === 'ok'; setBlockClass('block_browser', browserOk ? 'status-ok' : 'status-error'); setBlockClass('block_cloudflare', cfOk ? 'status-ok' : 'status-error'); setBlockClass('block_host', hostOk ? 'status-ok' : 'status-error'); } function setBlockClass(id, cls) { const el = $(id); if (!el) return; el.classList.remove('status-ok', 'status-error'); el.classList.add(cls); } /* Wire up events */ // initialize form values from initialConfig loadConfig(initialConfig); render(); // On preset change, load preset and render $('presetSelect').addEventListener('change', (e) => { const p = e.target.value; if (PRESETS[p]) loadConfig(PRESETS[p]); render(); }); // Render / Open button handlers // $('btnRender').addEventListener('click', e => { e.preventDefault(); render(); }); $('btnOpen').addEventListener('click', (e) => { openInNewTab(); }); $('btnShare').addEventListener('click', (e) => { createShareableLink(); }); const shareLinkPopover = new Popover($('btnCopyLink')); $('btnCopyLink').addEventListener('click', () => { const field = $('shareLink'); field.select(); navigator.clipboard.writeText(field.value).then(() => { shareLinkPopover.show(); setTimeout(() => { shareLinkPopover.hide(); }, 2000); }); }); // Input change -> render const inputs = document.querySelectorAll('#editorForm input, #editorForm textarea, #editorForm select'); inputs.forEach((inp) => { inp.addEventListener('input', () => render()); // for radio change events (error_source) if (inp.type === 'radio') inp.addEventListener('change', () => render()); }); // Automatically update frame height const observer = new ResizeObserver((entries) => resizePreviewFrame()); const iframe = $('previewFrame'); observer.observe(iframe.contentWindow.document.body); // resizePreviewFrame() setInterval(resizePreviewFrame, 1000); // TODO... function saveFile(content, saveName) { const file = new File([content], saveName, { type: 'text/plain', }); const url = URL.createObjectURL(file); const link = document.createElement('a'); link.href = url; link.download = file.name; document.body.appendChild(link); link.click(); document.body.removeChild(link); window.URL.revokeObjectURL(url); } let saveAsType; let saveAsContent; function updateSaveAsDialog(e) { if (e) { const target = e.target; saveAsType = target.dataset.type; } else { saveAsType = 'json'; } let codegen; switch (saveAsType) { case 'js': codegen = jsCodeGen; break; case 'json': codegen = jsonCodeGen; break; case 'python': codegen = pythonCodeGen; break; } const params = { ...lastCfg }; delete params.time; if (codegen) { saveAsContent = codegen.generate(params); } else if (saveAsType == 'static') { render() // rerender the page saveAsContent = lastRenderedHtml; } else { throw new Error('unexpected saveAsType=' + saveAsType) } $('saveAsDialogCode').innerHTML = saveAsContent; $('saveAsDialogCode').scrollTop = 0; document.querySelectorAll('#saveAsDialogTypes button').forEach((element) => { const isCurrent = element.dataset.type == saveAsType; if (isCurrent) { element.classList.add('active'); } else { element.classList.remove('active'); } element.ariaCurrent = isCurrent; }); } $('saveAsDialog').addEventListener('show.bs.modal', (e) => { updateSaveAsDialog(); }); document.querySelectorAll('#saveAsDialogTypes button').forEach((element) => { element.addEventListener('click', updateSaveAsDialog); }); const saveAsDialogCopyPopover = new Popover($('saveAsDialogCopyBtn')); $('saveAsDialogCopyBtn').addEventListener('click', (e) => { const field = $('saveAsDialogCode'); field.select(); // field.setSelectionRange(0, field.value.length); navigator.clipboard.writeText(field.value).then(() => { saveAsDialogCopyPopover.show(); setTimeout(() => { saveAsDialogCopyPopover.hide(); }, 2000); }); }); $('saveAsDialogSaveBtn').addEventListener('click', (e) => { let saveName = ''; switch (saveAsType) { case 'js': saveName = 'cf-error-page-example.js'; break; case 'json': saveName = 'cf-error-page-params.json'; break; case 'python': saveName = 'cf_error_page_example.py'; break; case 'static': saveName = 'cf_error_page.html'; break; // TODO: name output files using page title } saveFile(saveAsContent, saveName); });