diff --git a/site/dashboard.html b/site/dashboard.html index a7734c9..04ed80d 100644 --- a/site/dashboard.html +++ b/site/dashboard.html @@ -1086,7 +1086,10 @@ async function removeAllowlistDomain(domain) { } catch (err) {} } +let editingRoute = false; + function renderServices(entries) { + if (editingRoute) return; const el = document.getElementById('servicesList'); if (!entries.length) { el.innerHTML = '
No services configured
'; @@ -1098,13 +1101,16 @@ function renderServices(entries) { ? 'LAN' : 'local only') : ''; + const esc = s => s.replace(/'/g, "\\'").replace(/"/g, '"'); const routeLines = (e.routes || []).map(r => - `
` + + `
` + `${r.path} ` + `→ :${r.port}` + (r.strip ? ` (strip)` : '') + + (e.name === 'numa' ? '' : ` `) + `
` ).join(''); + const deletable = e.source !== 'config' && e.name !== 'numa'; return `
@@ -1112,12 +1118,52 @@ function renderServices(entries) {
${e.name}.numa${lanBadge}
localhost:${e.target_port} → proxied
${routeLines} + ${e.name === 'numa' ? '' : `
`}
- ${e.name === 'numa' ? '' : ``} + ${deletable ? `` : ''}
`}).join(''); } +function toggleRouteForm(name) { + const el = document.getElementById('routeForm-' + name); + const opening = el.style.display === 'none'; + el.style.display = opening ? 'block' : 'none'; + editingRoute = opening; +} + +async function addRoute(name) { + const errEl = document.getElementById('routeError-' + name); + errEl.style.display = 'none'; + try { + const path = document.getElementById('routePath-' + name).value.trim(); + const port = parseInt(document.getElementById('routePort-' + name).value) || 0; + const strip = document.getElementById('routeStrip-' + name).checked; + const res = await fetch(API + '/services/' + encodeURIComponent(name) + '/routes', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ path, port, strip }), + }); + if (!res.ok) throw new Error(await res.text()); + editingRoute = false; + refresh(); + } catch (err) { + errEl.textContent = err.message; + errEl.style.display = 'block'; + } +} + +async function deleteRoute(name, path) { + try { + await fetch(API + '/services/' + encodeURIComponent(name) + '/routes', { + method: 'DELETE', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ path }), + }); + refresh(); + } catch (err) { /* next refresh will update */ } +} + async function addService(event) { event.preventDefault(); const errEl = document.getElementById('serviceError'); diff --git a/src/api.rs b/src/api.rs index 069c156..5696d70 100644 --- a/src/api.rs +++ b/src/api.rs @@ -601,6 +601,7 @@ struct ServiceResponse { lan_accessible: bool, #[serde(skip_serializing_if = "Vec::is_empty")] routes: Vec, + source: String, } #[derive(Deserialize)] @@ -615,7 +616,19 @@ async fn list_services(State(ctx): State>) -> Json>) -> Json = entries .iter() - .map(|(_, port, _)| { + .map(|(_, port, _, _)| { let port = *port; let localhost = std::net::SocketAddr::from(([127, 0, 0, 1], port)); let lan_addr = lan_ip.map(|ip| std::net::SocketAddr::new(ip.into(), port)); @@ -644,13 +657,14 @@ async fn list_services(State(ctx): State>) -> Json bool { + self.config_services.contains(name) + } + pub fn list(&self) -> Vec<&ServiceEntry> { let mut entries: Vec<_> = self.entries.values().collect(); entries.sort_by(|a, b| a.name.cmp(&b.name));