use std::sync::LazyLock;
pub static WHITELABEL: LazyLock<WhitelabelConfig> = LazyLock::new(WhitelabelConfig::from_env);
#[derive(Debug, Clone)]
pub struct WhitelabelConfig {
pub name: String,
pub mark: String,
pub subtitle: String,
pub logo_url: Option<String>,
pub page_title: String,
pub parent_url: Option<String>,
pub parent_name: String,
pub api_docs_url: Option<String>,
pub css_url: Option<String>,
pub favicon_url: Option<String>,
pub default_namespace: String,
}
impl WhitelabelConfig {
pub fn is_customised(&self) -> bool {
self.name != "Assay"
|| !self.subtitle.is_empty()
|| self.logo_url.is_some()
|| self.css_url.is_some()
|| self.favicon_url.is_some()
}
}
impl WhitelabelConfig {
pub fn from_env() -> Self {
let name =
std::env::var("ASSAY_WHITELABEL_NAME").unwrap_or_else(|_| "Assay".to_string());
let mark = std::env::var("ASSAY_WHITELABEL_MARK")
.ok()
.filter(|s| !s.is_empty())
.unwrap_or_else(|| {
name.chars()
.next()
.map(|c| c.to_uppercase().to_string())
.unwrap_or_else(|| "A".to_string())
});
let subtitle =
std::env::var("ASSAY_WHITELABEL_SUBTITLE").unwrap_or_default();
let logo_url = std::env::var("ASSAY_WHITELABEL_LOGO_URL")
.ok()
.filter(|s| !s.is_empty());
let page_title = std::env::var("ASSAY_WHITELABEL_PAGE_TITLE")
.unwrap_or_else(|_| "Assay Workflow Dashboard".to_string());
let parent_url = std::env::var("ASSAY_WHITELABEL_PARENT_URL")
.ok()
.filter(|s| !s.is_empty());
let parent_name =
std::env::var("ASSAY_WHITELABEL_PARENT_NAME").unwrap_or_else(|_| "Back".to_string());
let api_docs_url = match std::env::var("ASSAY_WHITELABEL_API_DOCS_URL") {
Ok(s) if s.is_empty() => None,
Ok(s) => Some(s),
Err(_) => Some("/api/v1/docs".to_string()),
};
let css_url = std::env::var("ASSAY_WHITELABEL_CSS_URL")
.ok()
.filter(|s| !s.is_empty());
let favicon_url = std::env::var("ASSAY_WHITELABEL_FAVICON_URL")
.ok()
.filter(|s| !s.is_empty());
let default_namespace = std::env::var("ASSAY_WHITELABEL_DEFAULT_NAMESPACE")
.ok()
.filter(|s| !s.is_empty())
.unwrap_or_else(|| "main".to_string());
Self {
name,
mark,
subtitle,
logo_url,
page_title,
parent_url,
parent_name,
api_docs_url,
css_url,
favicon_url,
default_namespace,
}
}
}
fn html_escape(s: &str) -> String {
s.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
.replace('"', """)
.replace('\'', "'")
}
pub fn render_index(template: &str, asset_version: &str, wl: &WhitelabelConfig) -> String {
let back_link = match &wl.parent_url {
Some(url) => format!(
r#"<a href="{}" class="nav-link nav-link-back" title="{}">
<span class="nav-icon">←</span> <span class="nav-label">{}</span>
</a>"#,
html_escape(url),
html_escape(&wl.parent_name),
html_escape(&wl.parent_name)
),
None => String::new(),
};
let api_docs_link = match &wl.api_docs_url {
Some(url) => format!(
r#"<a href="{}" class="nav-link nav-link-grow" target="_blank">
<span class="nav-icon">📄</span> <span class="nav-label">API Docs</span>
</a>"#,
html_escape(url)
),
None => String::new(),
};
let logo_img = match &wl.logo_url {
Some(url) => format!(
r#"<img class="logo-img" src="{}" alt="{}" />"#,
html_escape(url),
html_escape(&wl.name)
),
None => String::new(),
};
let subtitle = if wl.subtitle.is_empty() {
String::new()
} else {
format!(
r#"<span class="logo-subtitle">{}</span>"#,
html_escape(&wl.subtitle)
)
};
let engine_footer = if wl.is_customised() {
r#"Powered by <a class="assay-attribution" href="https://assay.rs" target="_blank" rel="noopener noreferrer">Assay</a> <span id="status-version">—</span>"#.to_string()
} else {
r#"Assay Workflow Engine <span id="status-version">—</span>"#.to_string()
};
let favicon_link = match &wl.favicon_url {
Some(url) => format!(
r#"<link rel="icon" href="{}">"#,
html_escape(url)
),
None => r#"<link rel="icon" type="image/svg+xml" href="/workflow/favicon.svg">"#.to_string(),
};
let default_namespace_attr = format!(
r#" data-default-namespace="{}""#,
html_escape(&wl.default_namespace)
);
let extra_css = match &wl.css_url {
Some(url) => {
let sep = if url.contains('?') { '&' } else { '?' };
format!(
r#"<link rel="stylesheet" href="{}{}v={}">"#,
html_escape(url),
sep,
asset_version
)
}
None => String::new(),
};
template
.replace("__ASSETV__", asset_version)
.replace("__PAGE_TITLE__", &html_escape(&wl.page_title))
.replace("__BRAND_NAME__", &html_escape(&wl.name))
.replace("__BRAND_MARK__", &html_escape(&wl.mark))
.replace("__BRAND_LOGO_IMG__", &logo_img)
.replace("__BRAND_SUBTITLE__", &subtitle)
.replace("__ENGINE_FOOTER__", &engine_footer)
.replace("__PARENT_BACK_LINK__", &back_link)
.replace("__API_DOCS_LINK__", &api_docs_link)
.replace("__EXTRA_CSS_LINK__", &extra_css)
.replace("__FAVICON_LINK__", &favicon_link)
.replace("__DEFAULT_NAMESPACE_ATTR__", &default_namespace_attr)
}
#[cfg(test)]
mod tests {
use super::*;
const TEMPLATE: &str = r#"<title>__PAGE_TITLE__</title>
<head>__FAVICON_LINK__ __EXTRA_CSS_LINK__</head>
<body__DEFAULT_NAMESPACE_ATTR__>
<span>__BRAND_NAME__</span><span>__BRAND_MARK__</span>
__BRAND_SUBTITLE__
__BRAND_LOGO_IMG__
__PARENT_BACK_LINK__
__API_DOCS_LINK__
<footer>__ENGINE_FOOTER__</footer>
v=__ASSETV__
</body>"#;
fn default_cfg() -> WhitelabelConfig {
WhitelabelConfig {
name: "Assay".into(),
mark: "A".into(),
subtitle: String::new(),
logo_url: None,
page_title: "Assay Workflow Dashboard".into(),
parent_url: None,
parent_name: "Back".into(),
api_docs_url: Some("/api/v1/docs".into()),
css_url: None,
favicon_url: None,
default_namespace: "main".into(),
}
}
#[test]
fn default_render_matches_standalone_identity() {
let out = render_index(TEMPLATE, "42", &default_cfg());
assert!(out.contains("<title>Assay Workflow Dashboard</title>"));
assert!(out.contains("<span>Assay</span><span>A</span>"));
assert!(!out.contains("nav-link-back"));
assert!(!out.contains("logo-img"));
assert!(!out.contains("logo-subtitle"));
assert!(out.contains("Assay Workflow Engine"));
assert!(!out.contains("Powered by"));
assert!(!out.contains("assay-attribution"));
assert!(out.contains("href=\"/api/v1/docs\""));
assert!(out.contains(r#"href="/workflow/favicon.svg""#));
assert!(out.contains(r#"data-default-namespace="main""#));
assert!(out.contains("v=42"));
}
#[test]
fn subtitle_renders_as_muted_span_when_set() {
let mut cfg = default_cfg();
cfg.name = "SIMONS".into();
cfg.subtitle = "Command Center".into();
let out = render_index(TEMPLATE, "v", &cfg);
assert!(out.contains(r#"<span class="logo-subtitle">Command Center</span>"#));
}
#[test]
fn subtitle_unset_emits_nothing() {
let out = render_index(TEMPLATE, "v", &default_cfg());
assert!(!out.contains("logo-subtitle"));
}
#[test]
fn mark_override_wins_over_name_initial() {
let mut cfg = default_cfg();
cfg.name = "The Platform".into();
cfg.mark = "P".into();
let out = render_index(TEMPLATE, "v", &cfg);
assert!(out.contains("<span>The Platform</span><span>P</span>"));
}
#[test]
fn whitelabel_footer_includes_powered_by_and_attribution_link() {
let mut cfg = default_cfg();
cfg.name = "Acme Workflows".into();
cfg.mark = "A".into();
let out = render_index(TEMPLATE, "v", &cfg);
assert!(out.contains("Powered by"));
assert!(out.contains(r#">Assay</a>"#), "short 'Assay' attribution text");
assert!(!out.contains("Workflow Engine</a>"), "should not say 'Assay Workflow Engine' in link");
assert!(out.contains(r#"href="https://assay.rs""#));
assert!(out.contains(r#"target="_blank""#));
assert!(out.contains(r#"rel="noopener noreferrer""#));
assert!(out.contains(r#"<span id="status-version">—</span>"#));
}
#[test]
fn favicon_url_override_emits_operator_link_tag() {
let mut cfg = default_cfg();
cfg.favicon_url = Some("/static/acme-favicon.ico".into());
let out = render_index(TEMPLATE, "v", &cfg);
assert!(out.contains(r#"href="/static/acme-favicon.ico""#));
assert!(!out.contains(r#"href="/workflow/favicon.svg""#));
}
#[test]
fn default_namespace_override_threads_into_data_attr() {
let mut cfg = default_cfg();
cfg.default_namespace = "deployments".into();
let out = render_index(TEMPLATE, "v", &cfg);
assert!(out.contains(r#"data-default-namespace="deployments""#));
}
#[test]
fn favicon_override_flips_footer_attribution_too() {
let mut cfg = default_cfg();
cfg.favicon_url = Some("/f.ico".into());
let out = render_index(TEMPLATE, "v", &cfg);
assert!(out.contains("Powered by"));
}
#[test]
fn customised_detection_requires_more_than_defaults() {
let base = default_cfg();
assert!(!base.is_customised(), "stock defaults must not read as customised");
let mut with_name = default_cfg();
with_name.name = "Acme".into();
assert!(with_name.is_customised());
let mut with_subtitle = default_cfg();
with_subtitle.subtitle = "Something".into();
assert!(with_subtitle.is_customised());
let mut with_logo = default_cfg();
with_logo.logo_url = Some("/l.svg".into());
assert!(with_logo.is_customised());
let mut with_css = default_cfg();
with_css.css_url = Some("/t.css".into());
assert!(with_css.is_customised());
}
#[test]
fn whitelabel_name_and_mark_are_substituted() {
let mut cfg = default_cfg();
cfg.name = "Command Center".into();
cfg.mark = "C".into();
let out = render_index(TEMPLATE, "v", &cfg);
assert!(out.contains("<span>Command Center</span><span>C</span>"));
}
#[test]
fn logo_url_renders_img_tag() {
let mut cfg = default_cfg();
cfg.logo_url = Some("/static/siemens-logo.png".into());
cfg.name = "CC".into();
let out = render_index(TEMPLATE, "v", &cfg);
assert!(out.contains(r#"src="/static/siemens-logo.png""#));
assert!(out.contains(r#"alt="CC""#));
}
#[test]
fn parent_url_renders_back_link() {
let mut cfg = default_cfg();
cfg.parent_url = Some("https://command.example/".into());
cfg.parent_name = "Command Center".into();
let out = render_index(TEMPLATE, "v", &cfg);
assert!(out.contains(r#"href="https://command.example/""#));
assert!(out.contains("Command Center"));
assert!(out.contains("nav-link-back"));
}
#[test]
fn api_docs_empty_hides_the_link() {
let mut cfg = default_cfg();
cfg.api_docs_url = None;
let out = render_index(TEMPLATE, "v", &cfg);
assert!(!out.contains("API Docs"));
assert!(!out.contains("/api/v1/docs"));
}
#[test]
fn api_docs_override_retargets_the_link() {
let mut cfg = default_cfg();
cfg.api_docs_url = Some("https://docs.example/api".into());
let out = render_index(TEMPLATE, "v", &cfg);
assert!(out.contains(r#"href="https://docs.example/api""#));
assert!(out.contains("API Docs"));
}
#[test]
fn css_url_unset_emits_no_extra_stylesheet() {
let out = render_index(TEMPLATE, "42", &default_cfg());
assert!(
!out.contains("rel=\"stylesheet\""),
"no extra stylesheet should render when ASSAY_WHITELABEL_CSS_URL is unset"
);
}
#[test]
fn css_url_emits_cache_busted_link_tag() {
let mut cfg = default_cfg();
cfg.css_url = Some("/static/cc-theme.css".into());
let out = render_index(TEMPLATE, "42", &cfg);
assert!(out.contains(r#"<link rel="stylesheet" href="/static/cc-theme.css?v=42">"#));
}
#[test]
fn css_url_with_existing_query_string_uses_ampersand() {
let mut cfg = default_cfg();
cfg.css_url = Some("/static/cc-theme.css?rev=abc".into());
let out = render_index(TEMPLATE, "42", &cfg);
assert!(out.contains("href=\"/static/cc-theme.css?rev=abc&v=42\""));
}
#[test]
fn html_in_brand_name_is_escaped() {
let mut cfg = default_cfg();
cfg.name = "Acme <Inc>".into();
let out = render_index(TEMPLATE, "v", &cfg);
assert!(out.contains("Acme <Inc>"));
assert!(!out.contains("<Inc>"), "raw angle brackets must not land in the HTML");
}
}