Skip to main content

horto_os_ui_shared/ops/
catalog.rs

1//! Short descriptions for known Horto box services and container images.
2
3/// Human description for a status-API service link name (`Homepage`, `EVCC`, …).
4#[must_use]
5pub fn describe_service(name: &str) -> Option<&'static str> {
6    match normalize_key(name).as_str() {
7        "homepage" => Some("Box dashboard (gethomepage) for apps and widgets."),
8        "dockge" => Some("Compose stack manager for Docker apps on the box."),
9        "cockpit" => Some("Host admin console (packages, logs, storage, network)."),
10        "open-webui" | "openwebui" => Some("Local chat UI for on-box LLM backends."),
11        "evcc" => Some("Home energy manager (chargers, PV, battery)."),
12        "whisper" => Some("Speech-to-text service used by voice pipelines."),
13        "deepseek" => Some("Local LLM endpoint (often via Open-WebUI)."),
14        "piper" => Some("Text-to-speech engine (Wyoming / voice stack)."),
15        "openwakeword" | "openwakeword-wyoming" => {
16            Some("Wake-word detection for hands-free voice.")
17        }
18        "cloudflared" | "cloudflare-tunnel" | "cloudflare-tunnel-hos1" => {
19            Some("Outbound tunnel to expose selected box services.")
20        }
21        _ => None,
22    }
23}
24
25/// Human description from Docker container name and/or image reference.
26#[must_use]
27pub fn describe_container(names: &str, image: &str) -> Option<&'static str> {
28    for part in names.split(',') {
29        let key = normalize_key(part.trim().trim_start_matches('/'));
30        if let Some(desc) = describe_service(&key) {
31            return Some(desc);
32        }
33        if let Some(desc) = describe_by_alias(&key) {
34            return Some(desc);
35        }
36    }
37    describe_image(image)
38}
39
40fn describe_by_alias(key: &str) -> Option<&'static str> {
41    match key {
42        "wyoming-piper" | "piper" => describe_service("piper"),
43        "whisper-cv" => describe_service("whisper"),
44        "deepseek-npu" => describe_service("deepseek"),
45        "openwakeword-wyoming" => describe_service("openwakeword"),
46        _ => None,
47    }
48}
49
50fn describe_image(image: &str) -> Option<&'static str> {
51    let img = image.to_ascii_lowercase();
52    if img.contains("gethomepage/homepage") {
53        return describe_service("homepage");
54    }
55    if img.contains("louislam/dockge") {
56        return describe_service("dockge");
57    }
58    if img.contains("open-webui") {
59        return describe_service("open-webui");
60    }
61    if img.contains("evcc/evcc") {
62        return describe_service("evcc");
63    }
64    if img.contains("wyoming-piper") || img.contains("rhasspy/wyoming-piper") {
65        return describe_service("piper");
66    }
67    if img.contains("wyoming-openwakeword") {
68        return describe_service("openwakeword");
69    }
70    if img.contains("whisper") {
71        return describe_service("whisper");
72    }
73    if img.contains("deepseek") {
74        return describe_service("deepseek");
75    }
76    if img.contains("cloudflared") {
77        return describe_service("cloudflared");
78    }
79    if img.contains("nginx") {
80        return Some("HTTP front (demo or reverse proxy).");
81    }
82    None
83}
84
85fn normalize_key(raw: &str) -> String {
86    raw.trim()
87        .to_ascii_lowercase()
88        .chars()
89        .map(|c| if c.is_ascii_alphanumeric() { c } else { '-' })
90        .collect::<String>()
91        .split('-')
92        .filter(|s| !s.is_empty())
93        .collect::<Vec<_>>()
94        .join("-")
95}
96
97#[cfg(test)]
98mod tests {
99    use super::{describe_container, describe_service};
100
101    #[test]
102    fn known_services() {
103        assert!(describe_service("Homepage").unwrap().contains("dashboard"));
104        assert!(describe_service("Open-WebUI").is_some());
105        assert!(describe_service("unknown-x").is_none());
106    }
107
108    #[test]
109    fn known_containers_by_name_and_image() {
110        assert!(describe_container("homepage", "nginx:alpine")
111            .unwrap()
112            .contains("dashboard"));
113        assert!(
114            describe_container("x", "ghcr.io/gethomepage/homepage:latest")
115                .unwrap()
116                .contains("dashboard")
117        );
118        assert!(describe_container("weird", "busybox:latest").is_none());
119    }
120}