mirror of
https://github.com/donlon/cloudflare-error-page.git
synced 2025-12-22 16:29:29 +00:00
editor: move frontend folder -> web
This commit is contained in:
438
editor/web/src/index.js
Normal file
438
editor/web/src/index.js
Normal file
@@ -0,0 +1,438 @@
|
||||
/*
|
||||
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'
|
||||
|
||||
|
||||
let template = ejs.compile(templateContent);
|
||||
|
||||
// can be changed if specified by '?from=<name>'
|
||||
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 information',
|
||||
},
|
||||
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.substr(1), key);
|
||||
}
|
||||
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 blob = new Blob([lastRenderedHtml], { type: 'text/html' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
window.open(url, '_blank', 'noopener');
|
||||
// note that this url won't be revoked
|
||||
}
|
||||
|
||||
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 exportJSON() {
|
||||
let content = JSON.stringify(lastCfg, null, 4);
|
||||
const file = new File([content], 'cloudflare-error-page-params.json', {
|
||||
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);
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
/* Simple debounce */
|
||||
function debounce(fn, wait) {
|
||||
let t;
|
||||
return (...args) => {
|
||||
clearTimeout(t);
|
||||
t = setTimeout(() => fn(...args), wait);
|
||||
};
|
||||
}
|
||||
|
||||
/* 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) => {
|
||||
e.preventDefault();
|
||||
openInNewTab();
|
||||
});
|
||||
$('btnShare').addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
createShareableLink();
|
||||
});
|
||||
$('btnExport').addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
exportJSON();
|
||||
});
|
||||
|
||||
$('btnCopyLink').addEventListener('click', () => {
|
||||
const field = $('shareLink');
|
||||
field.select();
|
||||
field.setSelectionRange(0, field.value.length);
|
||||
navigator.clipboard.writeText(field.value).then(() => {
|
||||
// No notification required unless you want one
|
||||
});
|
||||
});
|
||||
|
||||
// Input change -> render
|
||||
const inputs = document.querySelectorAll('#editorForm input, #editorForm textarea, #editorForm select');
|
||||
inputs.forEach((inp) => {
|
||||
inp.addEventListener(
|
||||
'input',
|
||||
debounce(() => {
|
||||
// Update status block color classes for quick visual feedback in the editor
|
||||
render();
|
||||
}, 200)
|
||||
);
|
||||
// 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...
|
||||
Reference in New Issue
Block a user