use alloc::string::String;
use edge_http::io::Error;
use edge_http::io::server::Connection;
use embedded_io_async::{ErrorType, Read, Write};
const CONFIG_PAGE_CSS: &str = include_str!("config_page.css");
pub fn page_to_id(page: &str) -> String {
page.replace('-', "_").replace(' ', "_")
}
const CONFIG_PAGE_TAB_SCRIPT: &str = r#"
var messageEl = document.getElementById('message');
function showMessage(text, isError) {
if (!messageEl) return;
messageEl.textContent = text;
messageEl.className = 'message ' + (isError ? 'error' : 'success') + ' show';
setTimeout(function() { messageEl.classList.remove('show'); }, 5000);
}
var loaded = {};
function switchTab(page) {
document.querySelectorAll('.config-tab').forEach(function(b) { b.classList.remove('active'); });
document.querySelectorAll('.config-tab-panel').forEach(function(p) { p.style.display = 'none'; });
var btn = document.querySelector('.config-tab[data-page="' + page + '"]');
if (btn) btn.classList.add('active');
var panel = document.getElementById('panel-' + page);
if (panel) panel.style.display = '';
if (!loaded[page]) {
loaded[page] = true;
var loadFn = window['loadConfig_' + page];
if (loadFn) {
loadFn().then(function() {
panel.classList.add('loaded');
panel.querySelectorAll('input,button').forEach(function(el) { el.disabled = false; });
showMessage('Configuration loaded');
}).catch(function(err) {
var ov = document.getElementById('loading-' + page);
if (ov) ov.textContent = 'Load failed: ' + err.message;
var rb = panel.querySelector('.reloadBtn');
if (rb) rb.disabled = false;
});
}
}
}
document.querySelectorAll('.config-tab').forEach(function(btn) {
btn.addEventListener('click', function() { switchTab(this.dataset.page); });
});
document.querySelectorAll('.config-tab-panel').forEach(function(panel) {
panel.querySelectorAll('input,button').forEach(function(el) { el.disabled = true; });
});
document.querySelectorAll('.config-tab-panel form').forEach(function(form) {
form.addEventListener('submit', function(e) {
e.preventDefault();
var page = this.id.replace('configForm-', '');
var saveFn = window['saveConfig_' + page];
if (saveFn) saveFn().then(function() { showMessage('Configuration saved'); })
.catch(function(err) { showMessage('Save failed: ' + err.message, true); });
});
var rb = form.querySelector('.reloadBtn');
if (rb) rb.addEventListener('click', function() {
var page = this.closest('form').id.replace('configForm-', '');
var loadFn = window['loadConfig_' + page];
if (loadFn) loadFn().then(function() { showMessage('Configuration loaded'); })
.catch(function(e) { showMessage('Load failed: ' + e.message, true); });
});
});
"#;
const HTML_HEAD_OPEN: &str = r#"<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0"><title>"#;
const HTML_HEAD_TITLE_STYLE: &str = r#"</title><style>"#;
const HTML_HEAD_AFTER_STYLE: &str = r#"</style></head><body><div class="container"><header><h1>"#;
const HTML_HEAD_AFTER_H1: &str = r#"</h1><p>"#;
const HTML_SUBTITLE_NAV: &str = r#"</p></header><div class="nav">"#;
const HTML_CONTENT_OPEN: &str =
r#"</div><div class="content"><div id="message" class="message"></div>"#;
const HTML_SCRIPT_WRAPPER_END: &str = r#"</script></body></html>"#;
const HTML_SCRIPT_MIDDLE: &str = "</script><script>";
pub const EMPTY_SEGMENTS: &[&'static str] = &[];
pub struct PageTab {
pub name: &'static str,
pub html_segments: &'static [&'static str],
pub js_segments: &'static [&'static str],
}
pub struct ConfigPageChunks {
pub page_heading: &'static str,
pub title: &'static str,
pub subtitle: &'static str,
pub nav_left: &'static str,
pub nav_right: &'static str,
pub extra_css: &'static str,
pub pages: alloc::vec::Vec<PageTab>,
pub default_page_id: alloc::string::String,
}
impl ConfigPageChunks {
pub async fn write_to<T, const N: usize>(
self,
conn: &mut Connection<'_, T, N>,
) -> Result<(), Error<<T as ErrorType>::Error>>
where
T: Read + Write,
{
conn.initiate_response(
200,
None,
&[
("Content-Type", "text/html; charset=utf-8"),
("Connection", "close"),
],
)
.await?;
conn.write_all(HTML_HEAD_OPEN.as_bytes()).await?;
conn.write_all(self.title.as_bytes()).await?;
conn.write_all(HTML_HEAD_TITLE_STYLE.as_bytes()).await?;
conn.write_all(CONFIG_PAGE_CSS.as_bytes()).await?;
conn.write_all(self.extra_css.as_bytes()).await?;
conn.write_all(HTML_HEAD_AFTER_STYLE.as_bytes()).await?;
conn.write_all(self.page_heading.as_bytes()).await?;
conn.write_all(HTML_HEAD_AFTER_H1.as_bytes()).await?;
conn.write_all(self.subtitle.as_bytes()).await?;
conn.write_all(HTML_SUBTITLE_NAV.as_bytes()).await?;
conn.write_all(self.nav_left.as_bytes()).await?;
conn.write_all(self.nav_right.as_bytes()).await?;
conn.write_all(HTML_CONTENT_OPEN.as_bytes()).await?;
let show_tabs = self.pages.len() > 1;
if show_tabs {
let mut tab_html = String::from(r#"<div class="config-tabs">"#);
for tab in &self.pages {
let id = page_to_id(tab.name);
let active = if id == self.default_page_id {
" config-tab active"
} else {
" config-tab"
};
tab_html.push_str(r#"<button type="button" class=""#);
tab_html.push_str(active);
tab_html.push_str(r#"" data-page=""#);
tab_html.push_str(&id);
tab_html.push_str(r#"">"#);
tab_html.push_str(&escape_html(tab.name));
tab_html.push_str(r#"</button>"#);
}
tab_html.push_str("</div>");
conn.write_all(tab_html.as_bytes()).await?;
}
for tab in &self.pages {
let id = page_to_id(tab.name);
let display = if id == *self.default_page_id {
""
} else {
" style=\"display:none\""
};
let panel_open = alloc::format!(
r#"<div class="config-tab-panel" id="panel-{}"{}>"#,
id,
display
);
conn.write_all(panel_open.as_bytes()).await?;
let overlay = alloc::format!(
r#"<div class="config-loading-overlay" id="loading-{}"><span class="loading loading-overlay"></span>Loading...</div>"#,
id
);
conn.write_all(overlay.as_bytes()).await?;
let form_open = alloc::format!(r#"<form id="configForm-{}">"#, id);
conn.write_all(form_open.as_bytes()).await?;
for seg in tab.html_segments {
conn.write_all(seg.as_bytes()).await?;
}
let form_buttons = r#"<div class="button-group"><button type="button" class="reloadBtn">Reload</button><button type="submit">Save Configuration</button></div></form>"#;
conn.write_all(form_buttons.as_bytes()).await?;
conn.write_all(b"</div>").await?;
}
conn.write_all(HTML_SCRIPT_MIDDLE.as_bytes()).await?;
for tab in self.pages {
for seg in tab.js_segments {
conn.write_all(seg.as_bytes()).await?;
}
}
conn.write_all(HTML_SCRIPT_MIDDLE.as_bytes()).await?;
conn.write_all(CONFIG_PAGE_TAB_SCRIPT.as_bytes()).await?;
let default_page_script = alloc::format!(
"var defaultPage=\"{}\";window.addEventListener('load', function() {{ switchTab(defaultPage); }});\n",
escape_js(&self.default_page_id)
);
conn.write_all(default_page_script.as_bytes()).await?;
conn.write_all(HTML_SCRIPT_WRAPPER_END.as_bytes()).await?;
debug!("config_page: all chunks written, completing response");
let result = conn.complete().await;
match &result {
Ok(()) => info!("config_page: response complete OK"),
Err(_) => warn!("config_page: response complete FAILED"),
}
result
}
}
fn escape_html(s: &str) -> String {
let mut out = String::with_capacity(s.len() + 8);
for c in s.chars() {
match c {
'&' => out.push_str("&"),
'<' => out.push_str("<"),
'>' => out.push_str(">"),
'"' => out.push_str("""),
_ => out.push(c),
}
}
out
}
fn escape_js(s: &str) -> String {
let mut out = String::with_capacity(s.len() + 8);
for c in s.chars() {
match c {
'\\' => out.push_str("\\\\"),
'"' => out.push_str("\\\""),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
_ => out.push(c),
}
}
out
}