fedimint_server_ui/
dashboard.rs
1use std::future::Future;
2use std::net::SocketAddr;
3use std::pin::Pin;
4
5use axum::Router;
6use axum::extract::{Form, State};
7use axum::response::{Html, IntoResponse, Redirect};
8use axum::routing::{get, post};
9use axum_extra::extract::cookie::CookieJar;
10use fedimint_core::task::TaskHandle;
11use fedimint_server_core::dashboard_ui::{DashboardApiModuleExt, DynDashboardApi};
12use maud::{DOCTYPE, Markup, html};
13use tokio::net::TcpListener;
14use {fedimint_lnv2_server, fedimint_meta_server, fedimint_wallet_server};
15
16use crate::assets::WithStaticRoutesExt as _;
17use crate::layout::{self};
18use crate::{
19 AuthState, LoginInput, audit, check_auth, invite_code, latency, lnv2, login_form_response,
20 login_submit_response, meta, wallet,
21};
22
23pub fn dashboard_layout(content: Markup) -> Markup {
24 html! {
25 (DOCTYPE)
26 html {
27 head {
28 (layout::common_head("Dashboard"))
29 }
30 body {
31 div class="container" style="max-width: 66%;" {
32 header class="text-center" {
33 h1 class="header-title" { "Fedimint Guardian UI" }
34 }
35
36 (content)
37 }
38 script src="/assets/bootstrap.bundle.min.js" integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL" crossorigin="anonymous" {}
39 }
40 }
41 }
42}
43
44async fn login_form(State(_state): State<AuthState<DynDashboardApi>>) -> impl IntoResponse {
46 login_form_response()
47}
48
49async fn login_submit(
51 State(state): State<AuthState<DynDashboardApi>>,
52 jar: CookieJar,
53 Form(input): Form<LoginInput>,
54) -> impl IntoResponse {
55 login_submit_response(
56 state.api.auth().await,
57 state.auth_cookie_name,
58 state.auth_cookie_value,
59 jar,
60 input,
61 )
62 .into_response()
63}
64
65fn render_session_count(session_count: usize) -> Markup {
66 html! {
67
68 div id="session-count" class="alert alert-info" hx-swap-oob=(true) {
69 "Session Count: " strong { (session_count) }
70 }
71 }
72}
73async fn dashboard_view(
75 State(state): State<AuthState<DynDashboardApi>>,
76 jar: CookieJar,
77) -> impl IntoResponse {
78 if !check_auth(&state.auth_cookie_name, &state.auth_cookie_value, &jar).await {
79 return Redirect::to("/login").into_response();
80 }
81
82 let guardian_names = state.api.guardian_names().await;
83 let federation_name = state.api.federation_name().await;
84 let session_count = state.api.session_count().await;
85 let consensus_ord_latency = state.api.consensus_ord_latency().await;
86 let p2p_connection_status = state.api.p2p_connection_status().await;
87 let invite_code = state.api.federation_invite_code().await;
88 let audit_summary = state.api.federation_audit().await;
89
90 let lightning_content = html! {
92 @if let Some(lightning) = state.api.get_module::<fedimint_lnv2_server::Lightning>() {
93 (lnv2::render(lightning).await)
94 }
95 };
96
97 let wallet_content = html! {
99 @if let Some(wallet_module) = state.api.get_module::<fedimint_wallet_server::Wallet>() {
100 (wallet::render(wallet_module).await)
101 }
102 };
103
104 let meta_content = html! {
106 @if let Some(meta_module) = state.api.get_module::<fedimint_meta_server::Meta>() {
107 (meta::render(meta_module).await)
108 }
109 };
110
111 let content = html! {
112 div class="row gy-4" {
113 div class="col-md-6" {
114 div class="card h-100" {
115 div class="card-header dashboard-header" { (federation_name) }
116 div class="card-body" {
117 (render_session_count(session_count))
118 table class="table table-sm mb-0" {
119 thead {
120 tr {
121 th { "Guardian ID" }
122 th { "Guardian Name" }
123 }
124 }
125 tbody {
126 @for (guardian_id, name) in guardian_names {
127 tr {
128 td { (guardian_id.to_string()) }
129 td { (name) }
130 }
131 }
132 }
133 }
134 }
135 }
136 }
137
138 div class="col-md-6" {
140 (invite_code::invite_code_card())
141 }
142 }
143
144 (invite_code::invite_code_modal(&invite_code))
146
147 div class="row gy-4 mt-2" {
149 div class="col-lg-6" {
151 (audit::render(&audit_summary))
152 }
153
154 div class="col-lg-6" {
156 (latency::render(consensus_ord_latency, &p2p_connection_status))
157 }
158 }
159
160 (lightning_content)
161 (wallet_content)
162 (meta_content)
163
164 div hx-get="/dashboard/update" hx-trigger="every 15s" hx-swap="none" { }
166 };
167
168 Html(dashboard_layout(content).into_string()).into_response()
169}
170
171async fn dashboard_update(
176 State(state): State<AuthState<DynDashboardApi>>,
177 jar: CookieJar,
178) -> impl IntoResponse {
179 if !check_auth(&state.auth_cookie_name, &state.auth_cookie_value, &jar).await {
180 return Redirect::to("/login").into_response();
181 }
182
183 let session_count = state.api.session_count().await;
184 let consensus_ord_latency = state.api.consensus_ord_latency().await;
185 let p2p_connection_status = state.api.p2p_connection_status().await;
186
187 let content = html! {
190 (render_session_count(session_count))
191
192 (latency::render(consensus_ord_latency, &p2p_connection_status))
193
194 @if let Some(lightning) = state.api.get_module::<fedimint_lnv2_server::Lightning>() {
195 (lnv2::render(lightning).await)
196 }
197 };
198
199 Html(content.into_string()).into_response()
200}
201
202pub fn start(
203 api: DynDashboardApi,
204 ui_bind: SocketAddr,
205 task_handle: TaskHandle,
206) -> Pin<Box<dyn Future<Output = ()> + Send>> {
207 let mut app = Router::new()
209 .route("/", get(dashboard_view))
210 .route("/dashboard/update", get(dashboard_update))
211 .route("/login", get(login_form).post(login_submit))
212 .with_static_routes();
213
214 if api
216 .get_module::<fedimint_lnv2_server::Lightning>()
217 .is_some()
218 {
219 app = app
220 .route("/lnv2_gateway_add", post(lnv2::add_gateway))
221 .route("/lnv2_gateway_remove", post(lnv2::remove_gateway));
222 }
223
224 if api.get_module::<fedimint_meta_server::Meta>().is_some() {
226 app = app.route("/meta/submit", post(meta::submit_meta_value))
227 }
228
229 let app = app.with_state(AuthState::new(api));
231
232 Box::pin(async move {
233 let listener = TcpListener::bind(ui_bind)
234 .await
235 .expect("Failed to bind dashboard UI");
236
237 axum::serve(listener, app.into_make_service())
238 .with_graceful_shutdown(task_handle.make_shutdown_rx())
239 .await
240 .expect("Failed to serve dashboard UI");
241 })
242}