diff --git a/src/ui_server/mod.rs b/src/ui_server/mod.rs
index 1a9f6c1..5507e28 100644
--- a/src/ui_server/mod.rs
+++ b/src/ui_server/mod.rs
@@ -18,8 +18,7 @@ use log::{debug, error, info};
use serde::Deserialize;
use std::{fs, path::PathBuf};
use tokio_tungstenite::{
- connect_async_tls_with_config,
- tungstenite::protocol::Message as TungsteniteMessage,
+ connect_async_tls_with_config, tungstenite::protocol::Message as TungsteniteMessage,
};
use crate::shared::AppState;
@@ -99,7 +98,10 @@ async fn proxy_api(
req: Request
,
) -> Response {
let path = original_uri.path();
- let query = original_uri.query().map(|q| format!("?{}", q)).unwrap_or_default();
+ let query = original_uri
+ .query()
+ .map(|q| format!("?{}", q))
+ .unwrap_or_default();
let method = req.method().clone();
let headers = req.headers().clone();
@@ -217,7 +219,11 @@ async fn ws_proxy(
async fn handle_ws_proxy(client_socket: WebSocket, state: AppState, params: WsQuery) {
let backend_url = format!(
"{}/ws?session_id={}&user_id={}",
- state.client.base_url().replace("https://", "wss://").replace("http://", "ws://"),
+ state
+ .client
+ .base_url()
+ .replace("https://", "wss://")
+ .replace("http://", "ws://"),
params.session_id,
params.user_id
);
@@ -234,12 +240,8 @@ async fn handle_ws_proxy(client_socket: WebSocket, state: AppState, params: WsQu
let connector = tokio_tungstenite::Connector::NativeTls(tls_connector);
// Connect to backend WebSocket
- let backend_result = connect_async_tls_with_config(
- &backend_url,
- None,
- false,
- Some(connector),
- ).await;
+ let backend_result =
+ connect_async_tls_with_config(&backend_url, None, false, Some(connector)).await;
let backend_socket = match backend_result {
Ok((socket, _)) => socket,
@@ -260,22 +262,38 @@ async fn handle_ws_proxy(client_socket: WebSocket, state: AppState, params: WsQu
while let Some(msg) = client_rx.next().await {
match msg {
Ok(AxumMessage::Text(text)) => {
- if backend_tx.send(TungsteniteMessage::Text(text)).await.is_err() {
+ if backend_tx
+ .send(TungsteniteMessage::Text(text))
+ .await
+ .is_err()
+ {
break;
}
}
Ok(AxumMessage::Binary(data)) => {
- if backend_tx.send(TungsteniteMessage::Binary(data)).await.is_err() {
+ if backend_tx
+ .send(TungsteniteMessage::Binary(data))
+ .await
+ .is_err()
+ {
break;
}
}
Ok(AxumMessage::Ping(data)) => {
- if backend_tx.send(TungsteniteMessage::Ping(data)).await.is_err() {
+ if backend_tx
+ .send(TungsteniteMessage::Ping(data))
+ .await
+ .is_err()
+ {
break;
}
}
Ok(AxumMessage::Pong(data)) => {
- if backend_tx.send(TungsteniteMessage::Pong(data)).await.is_err() {
+ if backend_tx
+ .send(TungsteniteMessage::Pong(data))
+ .await
+ .is_err()
+ {
break;
}
}
@@ -323,8 +341,32 @@ async fn handle_ws_proxy(client_socket: WebSocket, state: AppState, params: WsQu
/// Create WebSocket proxy router
fn create_ws_router() -> Router {
+ Router::new().fallback(any(ws_proxy))
+}
+
+/// Create UI HTMX proxy router (for HTML fragment endpoints)
+fn create_ui_router() -> Router {
Router::new()
- .fallback(get(ws_proxy))
+ // Email UI endpoints
+ .route("/email/accounts", any(proxy_api))
+ .route("/email/list", any(proxy_api))
+ .route("/email/folders", any(proxy_api))
+ .route("/email/compose", any(proxy_api))
+ .route("/email/labels", any(proxy_api))
+ .route("/email/templates", any(proxy_api))
+ .route("/email/signatures", any(proxy_api))
+ .route("/email/rules", any(proxy_api))
+ .route("/email/search", any(proxy_api))
+ .route("/email/auto-responder", any(proxy_api))
+ .route("/email/{id}", any(proxy_api))
+ .route("/email/{id}/delete", any(proxy_api))
+ // Calendar UI endpoints
+ .route("/calendar/list", any(proxy_api))
+ .route("/calendar/upcoming", any(proxy_api))
+ .route("/calendar/event/new", any(proxy_api))
+ .route("/calendar/new", any(proxy_api))
+ // Fallback for any other /ui/* routes
+ .fallback(any(proxy_api))
}
/// Configure and return the main router
@@ -338,6 +380,8 @@ pub fn configure_router() -> Router {
.route("/health", get(health))
// API proxy routes
.nest("/api", create_api_router())
+ // UI HTMX proxy routes (for /ui/* endpoints that return HTML fragments)
+ .nest("/ui", create_ui_router())
// WebSocket proxy routes
.nest("/ws", create_ws_router())
// UI routes
@@ -373,6 +417,62 @@ pub fn configure_router() -> Router {
"/suite/tasks",
tower_http::services::ServeDir::new(suite_path.join("tasks")),
)
+ .nest_service(
+ "/suite/calendar",
+ tower_http::services::ServeDir::new(suite_path.join("calendar")),
+ )
+ .nest_service(
+ "/suite/meet",
+ tower_http::services::ServeDir::new(suite_path.join("meet")),
+ )
+ .nest_service(
+ "/suite/paper",
+ tower_http::services::ServeDir::new(suite_path.join("paper")),
+ )
+ .nest_service(
+ "/suite/research",
+ tower_http::services::ServeDir::new(suite_path.join("research")),
+ )
+ .nest_service(
+ "/suite/analytics",
+ tower_http::services::ServeDir::new(suite_path.join("analytics")),
+ )
+ .nest_service(
+ "/suite/monitoring",
+ tower_http::services::ServeDir::new(suite_path.join("monitoring")),
+ )
+ .nest_service(
+ "/suite/admin",
+ tower_http::services::ServeDir::new(suite_path.join("admin")),
+ )
+ .nest_service(
+ "/suite/auth",
+ tower_http::services::ServeDir::new(suite_path.join("auth")),
+ )
+ .nest_service(
+ "/suite/settings",
+ tower_http::services::ServeDir::new(suite_path.join("settings")),
+ )
+ .nest_service(
+ "/suite/sources",
+ tower_http::services::ServeDir::new(suite_path.join("sources")),
+ )
+ .nest_service(
+ "/suite/attendant",
+ tower_http::services::ServeDir::new(suite_path.join("attendant")),
+ )
+ .nest_service(
+ "/suite/tools",
+ tower_http::services::ServeDir::new(suite_path.join("tools")),
+ )
+ .nest_service(
+ "/suite/assets",
+ tower_http::services::ServeDir::new(suite_path.join("assets")),
+ )
+ .nest_service(
+ "/suite/partials",
+ tower_http::services::ServeDir::new(suite_path.join("partials")),
+ )
// Legacy paths for backward compatibility (serve suite assets)
.nest_service(
"/js",
diff --git a/ui/suite/admin/admin.css b/ui/suite/admin/admin.css
index 95a7fc0..8900c36 100644
--- a/ui/suite/admin/admin.css
+++ b/ui/suite/admin/admin.css
@@ -4,19 +4,20 @@
.admin-layout {
display: grid;
grid-template-columns: 260px 1fr;
- min-height: calc(100vh - 56px);
- background: var(--bg);
+ height: 100%;
+ min-height: 0;
+ background: var(--bg, var(--bg-primary, #0a0a0f));
+ color: var(--text, var(--text-primary, #ffffff));
}
/* Sidebar */
.admin-sidebar {
- background: var(--surface);
- border-right: 1px solid var(--border);
+ background: var(--surface, var(--bg-secondary, #12121a));
+ border-right: 1px solid var(--border, #2a2a3a);
display: flex;
flex-direction: column;
- position: sticky;
- top: 56px;
- height: calc(100vh - 56px);
+ overflow-y: auto;
+ height: 100%;
}
.admin-header {
@@ -138,10 +139,22 @@
justify-content: center;
}
-.stat-icon.users { background: rgba(59, 130, 246, 0.1); color: #3b82f6; }
-.stat-icon.groups { background: rgba(16, 185, 129, 0.1); color: #10b981; }
-.stat-icon.bots { background: rgba(168, 85, 247, 0.1); color: #a855f7; }
-.stat-icon.storage { background: rgba(249, 115, 22, 0.1); color: #f97316; }
+.stat-icon.users {
+ background: rgba(59, 130, 246, 0.1);
+ color: #3b82f6;
+}
+.stat-icon.groups {
+ background: rgba(16, 185, 129, 0.1);
+ color: #10b981;
+}
+.stat-icon.bots {
+ background: rgba(168, 85, 247, 0.1);
+ color: #a855f7;
+}
+.stat-icon.storage {
+ background: rgba(249, 115, 22, 0.1);
+ color: #f97316;
+}
.stat-content {
display: flex;
@@ -287,9 +300,15 @@
border-radius: 50%;
}
-.health-status.healthy { background: #10b981; }
-.health-status.warning { background: #f59e0b; }
-.health-status.error { background: #ef4444; }
+.health-status.healthy {
+ background: #10b981;
+}
+.health-status.warning {
+ background: #f59e0b;
+}
+.health-status.error {
+ background: #ef4444;
+}
.health-value {
font-size: 24px;
@@ -458,7 +477,9 @@
}
@keyframes spin {
- to { transform: rotate(360deg); }
+ to {
+ transform: rotate(360deg);
+ }
}
/* Responsive */
diff --git a/ui/suite/attendant/attendant.css b/ui/suite/attendant/attendant.css
index 665c74c..68f1e17 100644
--- a/ui/suite/attendant/attendant.css
+++ b/ui/suite/attendant/attendant.css
@@ -2,8 +2,10 @@
.attendant-container {
display: flex;
- height: calc(100vh - 60px);
- background: var(--bg);
+ height: 100%;
+ min-height: 0;
+ background: var(--bg, var(--bg-primary, #0a0a0f));
+ color: var(--text, var(--text-primary, #ffffff));
}
/* Queue Panel */
@@ -300,10 +302,18 @@
margin-right: 6px;
}
-.status-dot.online { background: #22c55e; }
-.status-dot.away { background: #f59e0b; }
-.status-dot.busy { background: #ef4444; }
-.status-dot.offline { background: #6b7280; }
+.status-dot.online {
+ background: #22c55e;
+}
+.status-dot.away {
+ background: #f59e0b;
+}
+.status-dot.busy {
+ background: #ef4444;
+}
+.status-dot.offline {
+ background: #6b7280;
+}
/* Responsive */
@media (max-width: 1024px) {
diff --git a/ui/suite/base.html b/ui/suite/base.html
index cee8d2b..2b0a2a8 100644
--- a/ui/suite/base.html
+++ b/ui/suite/base.html
@@ -362,182 +362,64 @@
Theme
-
+
@@ -730,7 +612,7 @@
{% block content %}{% endblock %}
-
+
-
+
-