horto_os_ui_shared/ops/
catalog.rs1#[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#[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}