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)]
@ -41,7 +41,6 @@ const SUITE_DIRS: &[&str] = &[
"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 = "mail"))]
#[cfg(not(feature = "meet"))] { html = remove_section(&html, "meet"); } {
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 // Documents
#[cfg(not(feature = "docs"))] { html = remove_section(&html, "docs"); } #[cfg(not(feature = "docs"))]
#[cfg(not(feature = "sheet"))] { html = remove_section(&html, "sheet"); } {
#[cfg(not(feature = "slides"))] { html = remove_section(&html, "slides"); } html = remove_section(&html, "docs");
#[cfg(not(feature = "paper"))] { html = remove_section(&html, "paper"); } }
#[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 // Research
#[cfg(not(feature = "research"))] { html = remove_section(&html, "research"); } #[cfg(not(feature = "research"))]
#[cfg(not(feature = "sources"))] { html = remove_section(&html, "sources"); } {
#[cfg(not(feature = "learn"))] { html = remove_section(&html, "learn"); } html = remove_section(&html, "research");
}
#[cfg(not(feature = "sources"))]
{
html = remove_section(&html, "sources");
}
#[cfg(not(feature = "learn"))]
{
html = remove_section(&html, "learn");
}
// Analytics // Analytics
#[cfg(not(feature = "analytics"))] { html = remove_section(&html, "analytics"); } #[cfg(not(feature = "analytics"))]
#[cfg(not(feature = "dashboards"))] { html = remove_section(&html, "dashboards"); } {
#[cfg(not(feature = "monitoring"))] { html = remove_section(&html, "monitoring"); } html = remove_section(&html, "analytics");
}
#[cfg(not(feature = "dashboards"))]
{
html = remove_section(&html, "dashboards");
}
#[cfg(not(feature = "monitoring"))]
{
html = remove_section(&html, "monitoring");
}
// Business // Business
#[cfg(not(feature = "people"))] { #[cfg(not(feature = "people"))]
{
html = remove_section(&html, "people"); html = remove_section(&html, "people");
html = remove_section(&html, "crm"); html = remove_section(&html, "crm");
} }
#[cfg(not(feature = "billing"))] { html = remove_section(&html, "billing"); } #[cfg(not(feature = "billing"))]
#[cfg(not(feature = "products"))] { html = remove_section(&html, "products"); } {
#[cfg(not(feature = "tickets"))] { html = remove_section(&html, "tickets"); } 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}");
( (
@ -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(),
} }
} }
@ -832,7 +955,8 @@ fn add_static_routes(router: Router<AppState>, _suite_path: &Path) -> Router<App
// 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!("/{}", file), get(handle_embedded_root_asset))
.route(&format!("/suite/{}", file), get(handle_embedded_root_asset)); .route(&format!("/suite/{}", file), get(handle_embedded_root_asset));
} }
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)
} }