fix(ui): improve asset discovery and error logging
Some checks failed
GBCI / build (push) Failing after 11s

This commit is contained in:
Rodrigo Rodriguez (Pragmatismo) 2026-01-26 14:50:27 -03:00
parent 12c1e3210f
commit 22fa29b3ec

View file

@ -1,4 +1,3 @@
use axum::{ use axum::{
body::Body, body::Body,
extract::{ extract::{
@ -12,17 +11,18 @@ use axum::{
}; };
use futures_util::{SinkExt, StreamExt}; use futures_util::{SinkExt, StreamExt};
use log::{debug, error, info}; use log::{debug, error, info};
#[cfg(feature = "embed-ui")]
use rust_embed::RustEmbed;
use serde::Deserialize; use serde::Deserialize;
#[cfg(not(feature = "embed-ui"))] #[cfg(not(feature = "embed-ui"))]
use std::fs; use std::fs;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use tokio_tungstenite::{ use tokio_tungstenite::{
connect_async_tls_with_config, tungstenite, tungstenite::protocol::Message as TungsteniteMessage, connect_async_tls_with_config, tungstenite,
tungstenite::protocol::Message as TungsteniteMessage,
}; };
#[cfg(not(feature = "embed-ui"))] #[cfg(not(feature = "embed-ui"))]
use tower_http::services::{ServeDir, ServeFile}; use tower_http::services::{ServeDir, ServeFile};
#[cfg(feature = "embed-ui")]
use rust_embed::RustEmbed;
#[cfg(feature = "embed-ui")] #[cfg(feature = "embed-ui")]
#[derive(RustEmbed)] #[derive(RustEmbed)]
@ -38,10 +38,9 @@ const SUITE_DIRS: &[&str] = &[
"assets", "assets",
"partials", "partials",
// Core & Support // Core & Support
"settings", "settings",
"auth", "auth",
"about", "about",
// Core Apps // Core Apps
#[cfg(feature = "drive")] #[cfg(feature = "drive")]
"drive", "drive",
@ -55,7 +54,6 @@ const SUITE_DIRS: &[&str] = &[
"calendar", "calendar",
#[cfg(feature = "meet")] #[cfg(feature = "meet")]
"meet", "meet",
// Document Apps // Document Apps
#[cfg(feature = "paper")] #[cfg(feature = "paper")]
"paper", "paper",
@ -65,7 +63,6 @@ const SUITE_DIRS: &[&str] = &[
"slides", "slides",
#[cfg(feature = "docs")] #[cfg(feature = "docs")]
"docs", "docs",
// Research & Learning // Research & Learning
#[cfg(feature = "research")] #[cfg(feature = "research")]
"research", "research",
@ -73,7 +70,6 @@ const SUITE_DIRS: &[&str] = &[
"sources", "sources",
#[cfg(feature = "learn")] #[cfg(feature = "learn")]
"learn", "learn",
// Analytics // Analytics
#[cfg(feature = "analytics")] #[cfg(feature = "analytics")]
"analytics", "analytics",
@ -81,7 +77,6 @@ const SUITE_DIRS: &[&str] = &[
"dashboards", "dashboards",
#[cfg(feature = "monitoring")] #[cfg(feature = "monitoring")]
"monitoring", "monitoring",
// Admin & Tools // Admin & Tools
#[cfg(feature = "admin")] #[cfg(feature = "admin")]
"admin", "admin",
@ -89,7 +84,6 @@ const SUITE_DIRS: &[&str] = &[
"attendant", "attendant",
#[cfg(feature = "tools")] #[cfg(feature = "tools")]
"tools", "tools",
// Media // Media
#[cfg(feature = "video")] #[cfg(feature = "video")]
"video", "video",
@ -97,7 +91,6 @@ const SUITE_DIRS: &[&str] = &[
"player", "player",
#[cfg(feature = "canvas")] #[cfg(feature = "canvas")]
"canvas", "canvas",
// Social // Social
#[cfg(feature = "social")] #[cfg(feature = "social")]
"social", "social",
@ -107,13 +100,11 @@ const SUITE_DIRS: &[&str] = &[
"crm", "crm",
#[cfg(feature = "tickets")] #[cfg(feature = "tickets")]
"tickets", "tickets",
// Business // Business
#[cfg(feature = "billing")] #[cfg(feature = "billing")]
"billing", "billing",
#[cfg(feature = "products")] #[cfg(feature = "products")]
"products", "products",
// Development // Development
#[cfg(feature = "designer")] #[cfg(feature = "designer")]
"designer", "designer",
@ -126,11 +117,18 @@ const SUITE_DIRS: &[&str] = &[
]; ];
const ROOT_FILES: &[&str] = &[ const ROOT_FILES: &[&str] = &[
"designer.html", "designer.css", "designer.js", "designer.html",
"editor.html", "editor.css", "editor.js", "designer.css",
"designer.js",
"editor.html",
"editor.css",
"editor.js",
"home.html", "home.html",
"base.html", "base-layout.html", "base-layout.css", "base.html",
"default.gbui", "single.gbui", "base-layout.html",
"base-layout.css",
"default.gbui",
"single.gbui",
]; ];
pub async fn index() -> impl IntoResponse { pub async fn index() -> impl IntoResponse {
@ -138,13 +136,30 @@ pub async fn index() -> impl IntoResponse {
} }
pub fn get_ui_root() -> PathBuf { pub fn get_ui_root() -> PathBuf {
if Path::new("ui").exists() { let candidates = [
PathBuf::from("ui") "ui",
} else if Path::new("botui/ui").exists() { "botui/ui",
PathBuf::from("botui/ui") "../botui/ui",
} else { "../../botui/ui",
PathBuf::from("ui") "../../../botui/ui",
];
for path_str in candidates {
let path = PathBuf::from(path_str);
if path.exists() {
info!("Found UI root at: {:?}", path);
return path;
}
} }
// Fallback to "ui" but log a warning
let default = PathBuf::from("ui");
error!(
"Could not find 'ui' directory in candidates: {:?}. Defaulting to 'ui' (CWD: {:?})",
candidates,
std::env::current_dir()
);
default
} }
pub async fn serve_minimal() -> impl IntoResponse { pub async fn serve_minimal() -> impl IntoResponse {
@ -157,8 +172,15 @@ pub async fn serve_minimal() -> impl IntoResponse {
} }
#[cfg(not(feature = "embed-ui"))] #[cfg(not(feature = "embed-ui"))]
{ {
fs::read_to_string(get_ui_root().join("minimal/index.html")) let path = get_ui_root().join("minimal/index.html");
.map_err(|e| e.to_string()) fs::read_to_string(&path).map_err(|e| {
format!(
"Failed to read {:?} (CWD: {:?}): {}",
path,
std::env::current_dir(),
e
)
})
} }
}; };
@ -185,7 +207,15 @@ pub async fn serve_suite() -> impl IntoResponse {
} }
#[cfg(not(feature = "embed-ui"))] #[cfg(not(feature = "embed-ui"))]
{ {
fs::read_to_string(get_ui_root().join("suite/index.html")).map_err(|e| e.to_string()) let path = get_ui_root().join("suite/index.html");
fs::read_to_string(&path).map_err(|e| {
format!(
"Failed to read {:?} (CWD: {:?}): {}",
path,
std::env::current_dir(),
e
)
})
} }
}; };
@ -195,64 +225,157 @@ pub async fn serve_suite() -> impl IntoResponse {
let mut html = raw_html; let mut html = raw_html;
// Core Apps // Core Apps
#[cfg(not(feature = "chat"))] { html = remove_section(&html, "chat"); } #[cfg(not(feature = "chat"))]
#[cfg(not(feature = "mail"))] { html = remove_section(&html, "mail"); } {
#[cfg(not(feature = "calendar"))] { html = remove_section(&html, "calendar"); } html = remove_section(&html, "chat");
#[cfg(not(feature = "drive"))] { html = remove_section(&html, "drive"); }
#[cfg(not(feature = "tasks"))] { html = remove_section(&html, "tasks"); }
#[cfg(not(feature = "meet"))] { html = remove_section(&html, "meet"); }
// Documents
#[cfg(not(feature = "docs"))] { html = remove_section(&html, "docs"); }
#[cfg(not(feature = "sheet"))] { html = remove_section(&html, "sheet"); }
#[cfg(not(feature = "slides"))] { html = remove_section(&html, "slides"); }
#[cfg(not(feature = "paper"))] { html = remove_section(&html, "paper"); }
// Research
#[cfg(not(feature = "research"))] { html = remove_section(&html, "research"); }
#[cfg(not(feature = "sources"))] { html = remove_section(&html, "sources"); }
#[cfg(not(feature = "learn"))] { html = remove_section(&html, "learn"); }
// Analytics
#[cfg(not(feature = "analytics"))] { html = remove_section(&html, "analytics"); }
#[cfg(not(feature = "dashboards"))] { html = remove_section(&html, "dashboards"); }
#[cfg(not(feature = "monitoring"))] { html = remove_section(&html, "monitoring"); }
// Business
#[cfg(not(feature = "people"))] {
html = remove_section(&html, "people");
html = remove_section(&html, "crm");
} }
#[cfg(not(feature = "billing"))] { html = remove_section(&html, "billing"); } #[cfg(not(feature = "mail"))]
#[cfg(not(feature = "products"))] { html = remove_section(&html, "products"); } {
#[cfg(not(feature = "tickets"))] { html = remove_section(&html, "tickets"); } html = remove_section(&html, "mail");
}
#[cfg(not(feature = "calendar"))]
{
html = remove_section(&html, "calendar");
}
#[cfg(not(feature = "drive"))]
{
html = remove_section(&html, "drive");
}
#[cfg(not(feature = "tasks"))]
{
html = remove_section(&html, "tasks");
}
#[cfg(not(feature = "meet"))]
{
html = remove_section(&html, "meet");
}
// Documents
#[cfg(not(feature = "docs"))]
{
html = remove_section(&html, "docs");
}
#[cfg(not(feature = "sheet"))]
{
html = remove_section(&html, "sheet");
}
#[cfg(not(feature = "slides"))]
{
html = remove_section(&html, "slides");
}
#[cfg(not(feature = "paper"))]
{
html = remove_section(&html, "paper");
}
// Research
#[cfg(not(feature = "research"))]
{
html = remove_section(&html, "research");
}
#[cfg(not(feature = "sources"))]
{
html = remove_section(&html, "sources");
}
#[cfg(not(feature = "learn"))]
{
html = remove_section(&html, "learn");
}
// Analytics
#[cfg(not(feature = "analytics"))]
{
html = remove_section(&html, "analytics");
}
#[cfg(not(feature = "dashboards"))]
{
html = remove_section(&html, "dashboards");
}
#[cfg(not(feature = "monitoring"))]
{
html = remove_section(&html, "monitoring");
}
// Business
#[cfg(not(feature = "people"))]
{
html = remove_section(&html, "people");
html = remove_section(&html, "crm");
}
#[cfg(not(feature = "billing"))]
{
html = remove_section(&html, "billing");
}
#[cfg(not(feature = "products"))]
{
html = remove_section(&html, "products");
}
#[cfg(not(feature = "tickets"))]
{
html = remove_section(&html, "tickets");
}
// Media // Media
#[cfg(not(feature = "video"))] { html = remove_section(&html, "video"); } #[cfg(not(feature = "video"))]
#[cfg(not(feature = "player"))] { html = remove_section(&html, "player"); } {
#[cfg(not(feature = "canvas"))] { html = remove_section(&html, "canvas"); } html = remove_section(&html, "video");
}
#[cfg(not(feature = "player"))]
{
html = remove_section(&html, "player");
}
#[cfg(not(feature = "canvas"))]
{
html = remove_section(&html, "canvas");
}
// Social & Project // Social & Project
#[cfg(not(feature = "social"))] { html = remove_section(&html, "social"); } #[cfg(not(feature = "social"))]
#[cfg(not(feature = "project"))] { html = remove_section(&html, "project"); } {
#[cfg(not(feature = "goals"))] { html = remove_section(&html, "goals"); } html = remove_section(&html, "social");
#[cfg(not(feature = "workspace"))] { html = remove_section(&html, "workspace"); } }
#[cfg(not(feature = "project"))]
{
html = remove_section(&html, "project");
}
#[cfg(not(feature = "goals"))]
{
html = remove_section(&html, "goals");
}
#[cfg(not(feature = "workspace"))]
{
html = remove_section(&html, "workspace");
}
// Admin/Tools // Admin/Tools
#[cfg(not(feature = "admin"))] { #[cfg(not(feature = "admin"))]
html = remove_section(&html, "admin"); {
html = remove_section(&html, "admin");
} }
// Mapped security to tools feature // Mapped security to tools feature
#[cfg(not(feature = "tools"))] { #[cfg(not(feature = "tools"))]
{
html = remove_section(&html, "security"); html = remove_section(&html, "security");
} }
#[cfg(not(feature = "attendant"))] { html = remove_section(&html, "attendant"); } #[cfg(not(feature = "attendant"))]
#[cfg(not(feature = "designer"))] { html = remove_section(&html, "designer"); } {
#[cfg(not(feature = "editor"))] { html = remove_section(&html, "editor"); } html = remove_section(&html, "attendant");
#[cfg(not(feature = "settings"))] { html = remove_section(&html, "settings"); } }
#[cfg(not(feature = "designer"))]
{
html = remove_section(&html, "designer");
}
#[cfg(not(feature = "editor"))]
{
html = remove_section(&html, "editor");
}
#[cfg(not(feature = "settings"))]
{
html = remove_section(&html, "settings");
}
(StatusCode::OK, [("content-type", "text/html")], Html(html)) (StatusCode::OK, [("content-type", "text/html")], Html(html))
}, }
Err(e) => { Err(e) => {
error!("Failed to load suite UI: {e}"); error!("Failed to load suite UI: {e}");
( (
@ -268,29 +391,29 @@ pub async fn serve_suite() -> impl IntoResponse {
pub fn remove_section(html: &str, section: &str) -> String { pub fn remove_section(html: &str, section: &str) -> String {
let start_marker = format!("<!-- SECTION:{} -->", section); let start_marker = format!("<!-- SECTION:{} -->", section);
let end_marker = format!("<!-- ENDSECTION:{} -->", section); let end_marker = format!("<!-- ENDSECTION:{} -->", section);
let mut result = String::with_capacity(html.len()); let mut result = String::with_capacity(html.len());
let mut current_pos = 0; let mut current_pos = 0;
// Process multiple occurrences of the section // Process multiple occurrences of the section
while let Some(start_idx) = html[current_pos..].find(&start_marker) { while let Some(start_idx) = html[current_pos..].find(&start_marker) {
let abs_start = current_pos + start_idx; let abs_start = current_pos + start_idx;
// Append content up to the marker // Append content up to the marker
result.push_str(&html[current_pos..abs_start]); result.push_str(&html[current_pos..abs_start]);
// Find end marker // Find end marker
if let Some(end_idx) = html[abs_start..].find(&end_marker) { if let Some(end_idx) = html[abs_start..].find(&end_marker) {
// Skip past the end marker // Skip past the end marker
current_pos = abs_start + end_idx + end_marker.len(); current_pos = abs_start + end_idx + end_marker.len();
} else { } else {
// No end marker? This shouldn't happen with our script, // No end marker? This shouldn't happen with our script,
// but if it does, just skip the start marker and continue // but if it does, just skip the start marker and continue
// or consume everything? // or consume everything?
// Safety: Skip start marker only // Safety: Skip start marker only
current_pos = abs_start + start_marker.len(); current_pos = abs_start + start_marker.len();
} }
} }
// Append remaining content // Append remaining content
result.push_str(&html[current_pos..]); result.push_str(&html[current_pos..]);
result result
@ -518,7 +641,9 @@ async fn handle_task_progress_ws_proxy(
let backend_result = let backend_result =
connect_async_tls_with_config(&backend_url, None, false, Some(connector)).await; connect_async_tls_with_config(&backend_url, None, false, Some(connector)).await;
let backend_socket: tokio_tungstenite::WebSocketStream<tokio_tungstenite::MaybeTlsStream<tokio::net::TcpStream>> = match backend_result { let backend_socket: tokio_tungstenite::WebSocketStream<
tokio_tungstenite::MaybeTlsStream<tokio::net::TcpStream>,
> = match backend_result {
Ok((socket, _)) => socket, Ok((socket, _)) => socket,
Err(e) => { Err(e) => {
error!("Failed to connect to backend task-progress WebSocket: {e}"); error!("Failed to connect to backend task-progress WebSocket: {e}");
@ -535,33 +660,29 @@ async fn handle_task_progress_ws_proxy(
while let Some(msg) = client_rx.next().await { while let Some(msg) = client_rx.next().await {
match msg { match msg {
Ok(AxumMessage::Text(text)) => { Ok(AxumMessage::Text(text)) => {
let res: Result<(), tungstenite::Error> = backend_tx let res: Result<(), tungstenite::Error> =
.send(TungsteniteMessage::Text(text)) backend_tx.send(TungsteniteMessage::Text(text)).await;
.await;
if res.is_err() { if res.is_err() {
break; break;
} }
} }
Ok(AxumMessage::Binary(data)) => { Ok(AxumMessage::Binary(data)) => {
let res: Result<(), tungstenite::Error> = backend_tx let res: Result<(), tungstenite::Error> =
.send(TungsteniteMessage::Binary(data)) backend_tx.send(TungsteniteMessage::Binary(data)).await;
.await;
if res.is_err() { if res.is_err() {
break; break;
} }
} }
Ok(AxumMessage::Ping(data)) => { Ok(AxumMessage::Ping(data)) => {
let res: Result<(), tungstenite::Error> = backend_tx let res: Result<(), tungstenite::Error> =
.send(TungsteniteMessage::Ping(data)) backend_tx.send(TungsteniteMessage::Ping(data)).await;
.await;
if res.is_err() { if res.is_err() {
break; break;
} }
} }
Ok(AxumMessage::Pong(data)) => { Ok(AxumMessage::Pong(data)) => {
let res: Result<(), tungstenite::Error> = backend_tx let res: Result<(), tungstenite::Error> =
.send(TungsteniteMessage::Pong(data)) backend_tx.send(TungsteniteMessage::Pong(data)).await;
.await;
if res.is_err() { if res.is_err() {
break; break;
} }
@ -572,13 +693,18 @@ async fn handle_task_progress_ws_proxy(
}; };
let backend_to_client = async { let backend_to_client = async {
while let Some(msg) = backend_rx.next().await as Option<Result<TungsteniteMessage, tungstenite::Error>> { while let Some(msg) =
backend_rx.next().await as Option<Result<TungsteniteMessage, tungstenite::Error>>
{
match msg { match msg {
Ok(TungsteniteMessage::Text(text)) => { Ok(TungsteniteMessage::Text(text)) => {
// Log manifest_update messages for debugging // Log manifest_update messages for debugging
let is_manifest = text.contains("manifest_update"); let is_manifest = text.contains("manifest_update");
if is_manifest { if is_manifest {
info!("[WS_PROXY] Forwarding manifest_update to client: {}...", &text[..text.len().min(200)]); info!(
"[WS_PROXY] Forwarding manifest_update to client: {}...",
&text[..text.len().min(200)]
);
} else if text.contains("task_progress") { } else if text.contains("task_progress") {
debug!("[WS_PROXY] Forwarding task_progress to client"); debug!("[WS_PROXY] Forwarding task_progress to client");
} }
@ -650,7 +776,9 @@ async fn handle_ws_proxy(client_socket: WebSocket, state: AppState, params: WsQu
let backend_result = let backend_result =
connect_async_tls_with_config(&backend_url, None, false, Some(connector)).await; connect_async_tls_with_config(&backend_url, None, false, Some(connector)).await;
let backend_socket: tokio_tungstenite::WebSocketStream<tokio_tungstenite::MaybeTlsStream<tokio::net::TcpStream>> = match backend_result { let backend_socket: tokio_tungstenite::WebSocketStream<
tokio_tungstenite::MaybeTlsStream<tokio::net::TcpStream>,
> = match backend_result {
Ok((socket, _)) => socket, Ok((socket, _)) => socket,
Err(e) => { Err(e) => {
error!("Failed to connect to backend WebSocket: {e}"); error!("Failed to connect to backend WebSocket: {e}");
@ -667,33 +795,29 @@ async fn handle_ws_proxy(client_socket: WebSocket, state: AppState, params: WsQu
while let Some(msg) = client_rx.next().await { while let Some(msg) = client_rx.next().await {
match msg { match msg {
Ok(AxumMessage::Text(text)) => { Ok(AxumMessage::Text(text)) => {
let res: Result<(), tungstenite::Error> = backend_tx let res: Result<(), tungstenite::Error> =
.send(TungsteniteMessage::Text(text)) backend_tx.send(TungsteniteMessage::Text(text)).await;
.await;
if res.is_err() { if res.is_err() {
break; break;
} }
} }
Ok(AxumMessage::Binary(data)) => { Ok(AxumMessage::Binary(data)) => {
let res: Result<(), tungstenite::Error> = backend_tx let res: Result<(), tungstenite::Error> =
.send(TungsteniteMessage::Binary(data)) backend_tx.send(TungsteniteMessage::Binary(data)).await;
.await;
if res.is_err() { if res.is_err() {
break; break;
} }
} }
Ok(AxumMessage::Ping(data)) => { Ok(AxumMessage::Ping(data)) => {
let res: Result<(), tungstenite::Error> = backend_tx let res: Result<(), tungstenite::Error> =
.send(TungsteniteMessage::Ping(data)) backend_tx.send(TungsteniteMessage::Ping(data)).await;
.await;
if res.is_err() { if res.is_err() {
break; break;
} }
} }
Ok(AxumMessage::Pong(data)) => { Ok(AxumMessage::Pong(data)) => {
let res: Result<(), tungstenite::Error> = backend_tx let res: Result<(), tungstenite::Error> =
.send(TungsteniteMessage::Pong(data)) backend_tx.send(TungsteniteMessage::Pong(data)).await;
.await;
if res.is_err() { if res.is_err() {
break; break;
} }
@ -761,7 +885,8 @@ async fn serve_favicon() -> impl IntoResponse {
StatusCode::OK, StatusCode::OK,
[("content-type", "image/x-icon")], [("content-type", "image/x-icon")],
content.data, content.data,
).into_response(), )
.into_response(),
None => StatusCode::NOT_FOUND.into_response(), None => StatusCode::NOT_FOUND.into_response(),
} }
} }
@ -769,11 +894,9 @@ async fn serve_favicon() -> impl IntoResponse {
{ {
let favicon_path = get_ui_root().join("suite/public/favicon.ico"); let favicon_path = get_ui_root().join("suite/public/favicon.ico");
match tokio::fs::read(&favicon_path).await { match tokio::fs::read(&favicon_path).await {
Ok(bytes) => ( Ok(bytes) => {
StatusCode::OK, (StatusCode::OK, [("content-type", "image/x-icon")], bytes).into_response()
[("content-type", "image/x-icon")], }
bytes,
).into_response(),
Err(_) => StatusCode::NOT_FOUND.into_response(), Err(_) => StatusCode::NOT_FOUND.into_response(),
} }
} }
@ -806,7 +929,7 @@ async fn handle_embedded_root_asset(
axum::extract::Path(filename): axum::extract::Path<String>, axum::extract::Path(filename): axum::extract::Path<String>,
) -> impl IntoResponse { ) -> impl IntoResponse {
if !ROOT_FILES.contains(&filename.as_str()) { if !ROOT_FILES.contains(&filename.as_str()) {
return StatusCode::NOT_FOUND.into_response(); return StatusCode::NOT_FOUND.into_response();
} }
let asset_path = format!("suite/{}", filename); let asset_path = format!("suite/{}", filename);
@ -829,11 +952,12 @@ fn add_static_routes(router: Router<AppState>, _suite_path: &Path) -> Router<App
let mut r = router let mut r = router
.route("/suite/:dir/*path", get(handle_embedded_asset)) .route("/suite/:dir/*path", get(handle_embedded_asset))
.route("/:dir/*path", get(handle_embedded_asset)); .route("/:dir/*path", get(handle_embedded_asset));
// Add root files // Add root files
for file in ROOT_FILES { for file in ROOT_FILES {
r = r.route(&format!("/{}", file), get(handle_embedded_root_asset)) r = r
.route(&format!("/suite/{}", file), get(handle_embedded_root_asset)); .route(&format!("/{}", file), get(handle_embedded_root_asset))
.route(&format!("/suite/{}", file), get(handle_embedded_root_asset));
} }
r r
} }
@ -846,7 +970,7 @@ fn add_static_routes(router: Router<AppState>, _suite_path: &Path) -> Router<App
.nest_service(&format!("/suite/{dir}"), ServeDir::new(path.clone())) .nest_service(&format!("/suite/{dir}"), ServeDir::new(path.clone()))
.nest_service(&format!("/{dir}"), ServeDir::new(path)); .nest_service(&format!("/{dir}"), ServeDir::new(path));
} }
for file in ROOT_FILES { for file in ROOT_FILES {
let path = _suite_path.join(file); let path = _suite_path.join(file);
r = r r = r
@ -874,7 +998,5 @@ pub fn configure_router() -> Router {
router = add_static_routes(router, &suite_path); router = add_static_routes(router, &suite_path);
router router.fallback(get(index)).with_state(state)
.fallback(get(index))
.with_state(state)
} }