pitchfork_cli/web/
helpers.rs1use crate::daemon::Daemon;
4use crate::daemon_id::DaemonId;
5use crate::procs::PROCS;
6
7pub fn html_escape(s: &str) -> String {
9 s.replace('&', "&")
10 .replace('<', "<")
11 .replace('>', ">")
12 .replace('"', """)
13 .replace('\'', "'")
14}
15
16pub fn url_encode(s: &str) -> String {
18 urlencoding::encode(s).into_owned()
19}
20
21pub fn css_safe_id(s: &str) -> String {
27 let mut result = String::with_capacity(s.len() * 2);
28 for c in s.chars() {
29 match c {
30 '/' => result.push_str("-2f-"),
31 '.' => result.push_str("-2e-"),
32 ':' => result.push_str("-3a-"),
33 '@' => result.push_str("-40-"),
34 '#' => result.push_str("-23-"),
35 '[' => result.push_str("-5b-"),
36 ']' => result.push_str("-5d-"),
37 '(' => result.push_str("-28-"),
38 ')' => result.push_str("-29-"),
39 '!' => result.push_str("-21-"),
40 '$' => result.push_str("-24-"),
41 '%' => result.push_str("-25-"),
42 '^' => result.push_str("-5e-"),
43 '&' => result.push_str("-26-"),
44 '*' => result.push_str("-2a-"),
45 '+' => result.push_str("-2b-"),
46 '=' => result.push_str("-3d-"),
47 '|' => result.push_str("-7c-"),
48 '\\' => result.push_str("-5c-"),
49 '~' => result.push_str("-7e-"),
50 '`' => result.push_str("-60-"),
51 '<' => result.push_str("-3c-"),
52 '>' => result.push_str("-3e-"),
53 ',' => result.push_str("-2c-"),
54 ' ' => result.push_str("-20-"),
55 '"' => result.push_str("-22-"),
56 '\'' => result.push_str("-27-"),
57 _ => result.push(c),
58 }
59 }
60 result
61}
62
63pub fn format_daemon_id_html(id: &DaemonId) -> String {
67 format!(
68 r#"<span class="daemon-ns">{}</span>/{}"#,
69 html_escape(id.namespace()),
70 html_escape(id.name())
71 )
72}
73
74pub fn daemon_row(id: &DaemonId, d: &Daemon, is_disabled: bool) -> String {
78 let id_str = id.to_string();
79 let safe_id = css_safe_id(&id_str);
80 let confirm_id = html_escape(&id_str); let url_id = url_encode(&id_str);
82 let display_html = format_daemon_id_html(id);
83 let status_class = match &d.status {
84 crate::daemon_status::DaemonStatus::Running => "running",
85 crate::daemon_status::DaemonStatus::Stopped => "stopped",
86 crate::daemon_status::DaemonStatus::Waiting => "waiting",
87 crate::daemon_status::DaemonStatus::Stopping => "stopping",
88 crate::daemon_status::DaemonStatus::Failed(_) => "failed",
89 crate::daemon_status::DaemonStatus::Errored(_) => "errored",
90 };
91
92 let pid_display = d
93 .pid
94 .map(|p| p.to_string())
95 .unwrap_or_else(|| "-".to_string());
96
97 let stats = d.pid.and_then(|pid| PROCS.get_stats(pid));
99 let cpu_display = stats
100 .map(|s| s.cpu_display())
101 .unwrap_or_else(|| "-".to_string());
102 let mem_display = stats
103 .map(|s| s.memory_display())
104 .unwrap_or_else(|| "-".to_string());
105 let uptime_display = stats
106 .map(|s| s.uptime_display())
107 .unwrap_or_else(|| "-".to_string());
108
109 let error_msg = html_escape(&d.status.error_message().unwrap_or_default());
110 let disabled_badge = if is_disabled {
111 r#"<span class="badge disabled">disabled</span>"#
112 } else {
113 ""
114 };
115
116 let actions = if d.status.is_running() {
117 format!(
118 r##"
119 <button hx-post="/daemons/{url_id}/stop" hx-target="#daemon-{safe_id}" hx-swap="outerHTML" hx-confirm="Stop daemon '{confirm_id}'?" class="btn btn-sm"><i data-lucide="square" class="icon"></i> Stop</button>
120 <button hx-post="/daemons/{url_id}/restart" hx-target="#daemon-{safe_id}" hx-swap="outerHTML" hx-confirm="Restart daemon '{confirm_id}'?" class="btn btn-sm"><i data-lucide="refresh-cw" class="icon"></i> Restart</button>
121 "##
122 )
123 } else {
124 format!(
125 r##"
126 <button hx-post="/daemons/{url_id}/start" hx-target="#daemon-{safe_id}" hx-swap="outerHTML" class="btn btn-sm btn-primary"><i data-lucide="play" class="icon"></i> Start</button>
127 "##
128 )
129 };
130
131 let toggle_btn = if is_disabled {
132 format!(
133 r##"<button hx-post="/daemons/{url_id}/enable" hx-target="#daemon-{safe_id}" hx-swap="outerHTML" class="btn btn-sm"><i data-lucide="check" class="icon"></i> Enable</button>"##
134 )
135 } else {
136 format!(
137 r##"<button hx-post="/daemons/{url_id}/disable" hx-target="#daemon-{safe_id}" hx-swap="outerHTML" hx-confirm="Disable daemon '{confirm_id}'?" class="btn btn-sm"><i data-lucide="x" class="icon"></i> Disable</button>"##
138 )
139 };
140
141 format!(
142 r#"<tr id="daemon-{safe_id}" class="clickable-row" onclick="window.location.href='/daemons/{url_id}'">
143 <td><a href="/daemons/{url_id}" class="daemon-name" onclick="event.stopPropagation()">{display_html}</a> {disabled_badge}</td>
144 <td>{pid_display}</td>
145 <td><span class="status {status_class}">{}</span></td>
146 <td>{cpu_display}</td>
147 <td>{mem_display}</td>
148 <td>{uptime_display}</td>
149 <td class="error-msg">{error_msg}</td>
150 <td class="actions" onclick="event.stopPropagation()">{actions} {toggle_btn} <a href="/logs/{url_id}" class="btn btn-sm"><i data-lucide="file-text" class="icon"></i> Logs</a></td>
151 </tr>"#,
152 d.status
153 )
154}