Update botui
This commit is contained in:
parent
1a50680712
commit
9a2596ed4a
7 changed files with 251 additions and 85 deletions
22
Cargo.toml
22
Cargo.toml
|
|
@ -4,6 +4,9 @@ version = "6.1.0"
|
|||
edition = "2021"
|
||||
description = "General Bots UI - Pure web interface"
|
||||
license = "AGPL-3.0"
|
||||
repository = "https://github.com/GeneralBots/BotServer"
|
||||
keywords = ["chatbot", "ui", "web-interface", "general-bots"]
|
||||
categories = ["web-programming", "gui"]
|
||||
|
||||
[dependencies.botlib]
|
||||
path = "../botlib"
|
||||
|
|
@ -49,3 +52,22 @@ tracing = "0.1"
|
|||
urlencoding = "2.1"
|
||||
uuid = { version = "1.11", features = ["serde", "v4"] }
|
||||
webbrowser = "0.8"
|
||||
|
||||
[lints.rust]
|
||||
unused_imports = "warn"
|
||||
unused_variables = "warn"
|
||||
unused_mut = "warn"
|
||||
unsafe_code = "deny"
|
||||
missing_debug_implementations = "warn"
|
||||
|
||||
[lints.clippy]
|
||||
all = "warn"
|
||||
pedantic = "warn"
|
||||
nursery = "warn"
|
||||
cargo = "warn"
|
||||
unwrap_used = "warn"
|
||||
expect_used = "warn"
|
||||
panic = "warn"
|
||||
todo = "warn"
|
||||
# Disabled: transitive dependencies we cannot control
|
||||
multiple_crate_versions = "allow"
|
||||
|
|
|
|||
127
PROMPT.md
127
PROMPT.md
|
|
@ -5,6 +5,118 @@
|
|||
|
||||
---
|
||||
|
||||
## ZERO TOLERANCE POLICY
|
||||
|
||||
**This project has the strictest code quality requirements possible.**
|
||||
|
||||
**EVERY SINGLE WARNING MUST BE FIXED. NO EXCEPTIONS.**
|
||||
|
||||
---
|
||||
|
||||
## ABSOLUTE PROHIBITIONS
|
||||
|
||||
```
|
||||
❌ NEVER use #![allow()] or #[allow()] in source code to silence warnings
|
||||
❌ NEVER use _ prefix for unused variables - DELETE the variable or USE it
|
||||
❌ NEVER use .unwrap() - use ? or proper error handling
|
||||
❌ NEVER use .expect() - use ? or proper error handling
|
||||
❌ NEVER use panic!() or unreachable!() - handle all cases
|
||||
❌ NEVER use todo!() or unimplemented!() - write real code
|
||||
❌ NEVER leave unused imports - DELETE them
|
||||
❌ NEVER leave dead code - DELETE it or IMPLEMENT it
|
||||
❌ NEVER use approximate constants (3.14159) - use std::f64::consts::PI
|
||||
❌ NEVER silence clippy in code - FIX THE CODE or configure in Cargo.toml
|
||||
❌ NEVER add comments explaining what code does - code must be self-documenting
|
||||
❌ NEVER use CDN links - all assets must be local
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## CARGO.TOML LINT EXCEPTIONS
|
||||
|
||||
When a clippy lint has **technical false positives** that cannot be fixed in code,
|
||||
disable it in `Cargo.toml` with a comment explaining why:
|
||||
|
||||
```toml
|
||||
[lints.clippy]
|
||||
# Disabled: has false positives for functions with mut self, heap types (Vec, String)
|
||||
missing_const_for_fn = "allow"
|
||||
# Disabled: Tauri commands require owned types (Window) that cannot be passed by reference
|
||||
needless_pass_by_value = "allow"
|
||||
# Disabled: transitive dependencies we cannot control
|
||||
multiple_crate_versions = "allow"
|
||||
```
|
||||
|
||||
**Approved exceptions:**
|
||||
- `missing_const_for_fn` - false positives for `mut self`, heap types
|
||||
- `needless_pass_by_value` - Tauri/framework requirements
|
||||
- `multiple_crate_versions` - transitive dependencies
|
||||
- `future_not_send` - when async traits require non-Send futures
|
||||
|
||||
---
|
||||
|
||||
## MANDATORY CODE PATTERNS
|
||||
|
||||
### Error Handling - Use `?` Operator
|
||||
|
||||
```rust
|
||||
// ❌ WRONG
|
||||
let value = something.unwrap();
|
||||
let value = something.expect("msg");
|
||||
|
||||
// ✅ CORRECT
|
||||
let value = something?;
|
||||
let value = something.ok_or_else(|| Error::NotFound)?;
|
||||
```
|
||||
|
||||
### Self Usage in Impl Blocks
|
||||
|
||||
```rust
|
||||
// ❌ WRONG
|
||||
impl MyStruct {
|
||||
fn new() -> MyStruct { MyStruct { } }
|
||||
}
|
||||
|
||||
// ✅ CORRECT
|
||||
impl MyStruct {
|
||||
fn new() -> Self { Self { } }
|
||||
}
|
||||
```
|
||||
|
||||
### Format Strings - Inline Variables
|
||||
|
||||
```rust
|
||||
// ❌ WRONG
|
||||
format!("Hello {}", name)
|
||||
|
||||
// ✅ CORRECT
|
||||
format!("Hello {name}")
|
||||
```
|
||||
|
||||
### Display vs ToString
|
||||
|
||||
```rust
|
||||
// ❌ WRONG
|
||||
impl ToString for MyType { }
|
||||
|
||||
// ✅ CORRECT
|
||||
impl std::fmt::Display for MyType { }
|
||||
```
|
||||
|
||||
### Derive Eq with PartialEq
|
||||
|
||||
```rust
|
||||
// ❌ WRONG
|
||||
#[derive(PartialEq)]
|
||||
struct MyStruct { }
|
||||
|
||||
// ✅ CORRECT
|
||||
#[derive(PartialEq, Eq)]
|
||||
struct MyStruct { }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Weekly Maintenance - EVERY MONDAY
|
||||
|
||||
### Package Review Checklist
|
||||
|
|
@ -435,13 +547,20 @@ ui/minimal/index.html # Minimal chat UI
|
|||
|
||||
## Remember
|
||||
|
||||
- **Two LLM modes**: Execution (fazer) vs Review (conferir)
|
||||
- **ZERO WARNINGS** - Every clippy warning must be fixed
|
||||
- **NO ALLOW IN CODE** - Never use #[allow()] in source files
|
||||
- **CARGO.TOML EXCEPTIONS OK** - Disable lints with false positives in Cargo.toml with comment
|
||||
- **NO DEAD CODE** - Delete unused code, never prefix with _
|
||||
- **NO UNWRAP/EXPECT** - Use ? operator or proper error handling
|
||||
- **INLINE FORMAT ARGS** - format!("{name}") not format!("{}", name)
|
||||
- **USE SELF** - In impl blocks, use Self not the type name
|
||||
- **DERIVE EQ** - Always derive Eq with PartialEq
|
||||
- **DISPLAY NOT TOSTRING** - Implement Display, not ToString
|
||||
- **USE DIAGNOSTICS** - Use IDE diagnostics tool, never call cargo clippy directly
|
||||
- **HTMX first**: Minimize JS, delegate to server
|
||||
- **Local assets**: No CDN, all vendor files local
|
||||
- **Dead code**: Never use _ prefix, implement real code
|
||||
- **cargo audit**: Must pass with 0 warnings
|
||||
- **No business logic**: All logic in botserver
|
||||
- **Feature gates**: Unused code never compiles
|
||||
- **HTML responses**: Server returns fragments, not JSON
|
||||
- **Version**: Always 6.1.0 - do not change without approval
|
||||
- **Theme system**: Use data-theme attribute on body, 6 themes available
|
||||
- **Session Continuation**: When running out of context, create detailed summary: (1) what was done, (2) what remains, (3) specific files and line numbers, (4) exact next steps.
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
//! BotUI - General Bots Pure Web UI
|
||||
//! `BotUI` - General Bots Pure Web UI
|
||||
//!
|
||||
//! This crate provides the web UI layer for General Bots:
|
||||
//! - Serves static HTMX UI files (suite, minimal)
|
||||
//! - Proxies API requests to botserver
|
||||
//! - WebSocket support for real-time communication
|
||||
//! - `WebSocket` support for real-time communication
|
||||
//!
|
||||
//! For desktop/mobile native features, see the `botapp` crate which
|
||||
//! wraps this pure web UI with Tauri.
|
||||
|
|
|
|||
31
src/main.rs
31
src/main.rs
|
|
@ -1,3 +1,7 @@
|
|||
//! `BotUI` main entry point
|
||||
//!
|
||||
//! Starts the web UI server for General Bots.
|
||||
|
||||
use log::info;
|
||||
use std::net::SocketAddr;
|
||||
|
||||
|
|
@ -21,14 +25,15 @@ fn get_port() -> u16 {
|
|||
async fn main() -> anyhow::Result<()> {
|
||||
init_logging();
|
||||
|
||||
info!("BotUI {} starting...", env!("CARGO_PKG_VERSION"));
|
||||
let version = env!("CARGO_PKG_VERSION");
|
||||
info!("BotUI {version} starting...");
|
||||
|
||||
let app = ui_server::configure_router();
|
||||
let port = get_port();
|
||||
let addr = SocketAddr::from(([0, 0, 0, 0], port));
|
||||
|
||||
let listener = tokio::net::TcpListener::bind(addr).await?;
|
||||
info!("UI server listening on http://{}", addr);
|
||||
info!("UI server listening on http://{addr}");
|
||||
|
||||
axum::serve(listener, app)
|
||||
.with_graceful_shutdown(shutdown_signal())
|
||||
|
|
@ -40,24 +45,28 @@ async fn main() -> anyhow::Result<()> {
|
|||
|
||||
async fn shutdown_signal() {
|
||||
let ctrl_c = async {
|
||||
tokio::signal::ctrl_c()
|
||||
.await
|
||||
.expect("Failed to install Ctrl+C handler");
|
||||
if let Err(e) = tokio::signal::ctrl_c().await {
|
||||
log::error!("Failed to install Ctrl+C handler: {e}");
|
||||
}
|
||||
};
|
||||
|
||||
#[cfg(unix)]
|
||||
let terminate = async {
|
||||
tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
|
||||
.expect("Failed to install SIGTERM handler")
|
||||
.recv()
|
||||
.await;
|
||||
match tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()) {
|
||||
Ok(mut signal) => {
|
||||
signal.recv().await;
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Failed to install SIGTERM handler: {e}");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
#[cfg(not(unix))]
|
||||
let terminate = std::future::pending::<()>();
|
||||
|
||||
tokio::select! {
|
||||
_ = ctrl_c => info!("Received Ctrl+C, shutting down..."),
|
||||
_ = terminate => info!("Received SIGTERM, shutting down..."),
|
||||
() = ctrl_c => info!("Received Ctrl+C, shutting down..."),
|
||||
() = terminate => info!("Received SIGTERM, shutting down..."),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
//! Shared types and state management for BotUI
|
||||
//! Shared types and state management for `BotUI`
|
||||
//!
|
||||
//! This module provides shared application state and utilities
|
||||
//! used across the UI server.
|
||||
|
|
|
|||
|
|
@ -1,22 +1,23 @@
|
|||
//! Application state management
|
||||
//!
|
||||
//! This module contains the shared application state that is passed to all
|
||||
//! route handlers and provides access to the BotServer client.
|
||||
//! route handlers and provides access to the `BotServer` client.
|
||||
|
||||
use botlib::http_client::BotServerClient;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Application state shared across all handlers
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct AppState {
|
||||
/// HTTP client for communicating with BotServer
|
||||
/// HTTP client for communicating with `BotServer`
|
||||
pub client: Arc<BotServerClient>,
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
/// Create a new application state
|
||||
///
|
||||
/// Uses BOTSERVER_URL environment variable if set, otherwise defaults to localhost:8080
|
||||
/// Uses `BOTSERVER_URL` environment variable if set, otherwise defaults to localhost:8080
|
||||
#[must_use]
|
||||
pub fn new() -> Self {
|
||||
let url = std::env::var("BOTSERVER_URL").ok();
|
||||
Self {
|
||||
|
|
@ -24,7 +25,7 @@ impl AppState {
|
|||
}
|
||||
}
|
||||
|
||||
/// Check if the BotServer is healthy
|
||||
/// Check if the `BotServer` is healthy
|
||||
pub async fn health_check(&self) -> bool {
|
||||
self.client.health_check().await
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,7 @@
|
|||
//! UI Server module for `BotUI`
|
||||
//!
|
||||
//! Handles HTTP routing, WebSocket proxying, and static file serving.
|
||||
|
||||
use axum::{
|
||||
body::Body,
|
||||
extract::{
|
||||
|
|
@ -12,7 +16,7 @@ use axum::{
|
|||
use futures_util::{SinkExt, StreamExt};
|
||||
use log::{debug, error, info};
|
||||
use serde::Deserialize;
|
||||
use std::{fs, path::PathBuf};
|
||||
use std::{fs, path::Path, path::PathBuf};
|
||||
use tokio_tungstenite::{
|
||||
connect_async_tls_with_config, tungstenite::protocol::Message as TungsteniteMessage,
|
||||
};
|
||||
|
|
@ -52,7 +56,7 @@ pub async fn serve_minimal() -> impl IntoResponse {
|
|||
match fs::read_to_string("ui/minimal/index.html") {
|
||||
Ok(html) => (StatusCode::OK, [("content-type", "text/html")], Html(html)),
|
||||
Err(e) => {
|
||||
error!("Failed to load minimal UI: {}", e);
|
||||
error!("Failed to load minimal UI: {e}");
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
[("content-type", "text/plain")],
|
||||
|
|
@ -66,7 +70,7 @@ pub async fn serve_suite() -> impl IntoResponse {
|
|||
match fs::read_to_string("ui/suite/index.html") {
|
||||
Ok(html) => (StatusCode::OK, [("content-type", "text/html")], Html(html)),
|
||||
Err(e) => {
|
||||
error!("Failed to load suite UI: {}", e);
|
||||
error!("Failed to load suite UI: {e}");
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
[("content-type", "text/plain")],
|
||||
|
|
@ -77,23 +81,24 @@ pub async fn serve_suite() -> impl IntoResponse {
|
|||
}
|
||||
|
||||
async fn health(State(state): State<AppState>) -> (StatusCode, axum::Json<serde_json::Value>) {
|
||||
match state.health_check().await {
|
||||
true => (
|
||||
if state.health_check().await {
|
||||
(
|
||||
StatusCode::OK,
|
||||
axum::Json(serde_json::json!({
|
||||
"status": "healthy",
|
||||
"service": "botui",
|
||||
"mode": "web"
|
||||
})),
|
||||
),
|
||||
false => (
|
||||
)
|
||||
} else {
|
||||
(
|
||||
StatusCode::SERVICE_UNAVAILABLE,
|
||||
axum::Json(serde_json::json!({
|
||||
"status": "unhealthy",
|
||||
"service": "botui",
|
||||
"error": "botserver unreachable"
|
||||
})),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -121,8 +126,7 @@ fn extract_app_context(headers: &axum::http::HeaderMap, path: &str) -> Option<St
|
|||
}
|
||||
}
|
||||
|
||||
if path.starts_with("/apps/") {
|
||||
let after_apps = &path[6..];
|
||||
if let Some(after_apps) = path.strip_prefix("/apps/") {
|
||||
if let Some(end) = after_apps.find('/') {
|
||||
return Some(after_apps[..end].to_string());
|
||||
}
|
||||
|
|
@ -139,18 +143,14 @@ async fn proxy_api(
|
|||
let path = original_uri.path();
|
||||
let query = original_uri
|
||||
.query()
|
||||
.map(|q| format!("?{}", q))
|
||||
.unwrap_or_default();
|
||||
.map_or_else(String::new, |q| format!("?{q}"));
|
||||
let method = req.method().clone();
|
||||
let headers = req.headers().clone();
|
||||
|
||||
let app_context = extract_app_context(&headers, path);
|
||||
|
||||
let target_url = format!("{}{}{}", state.client.base_url(), path, query);
|
||||
debug!(
|
||||
"Proxying {} {} to {} (app: {:?})",
|
||||
method, path, target_url, app_context
|
||||
);
|
||||
let target_url = format!("{}{path}{query}", state.client.base_url());
|
||||
debug!("Proxying {method} {path} to {target_url} (app: {app_context:?})");
|
||||
|
||||
let client = reqwest::Client::builder()
|
||||
.danger_accept_invalid_certs(true)
|
||||
|
|
@ -158,7 +158,7 @@ async fn proxy_api(
|
|||
.unwrap_or_else(|_| reqwest::Client::new());
|
||||
let mut proxy_req = client.request(method.clone(), &target_url);
|
||||
|
||||
for (name, value) in headers.iter() {
|
||||
for (name, value) in &headers {
|
||||
if name != "host" {
|
||||
if let Ok(v) = value.to_str() {
|
||||
proxy_req = proxy_req.header(name.as_str(), v);
|
||||
|
|
@ -173,11 +173,11 @@ async fn proxy_api(
|
|||
let body_bytes = match axum::body::to_bytes(req.into_body(), usize::MAX).await {
|
||||
Ok(bytes) => bytes,
|
||||
Err(e) => {
|
||||
error!("Failed to read request body: {}", e);
|
||||
return Response::builder()
|
||||
.status(StatusCode::INTERNAL_SERVER_ERROR)
|
||||
.body(Body::from("Failed to read request body"))
|
||||
.unwrap();
|
||||
error!("Failed to read request body: {e}");
|
||||
return build_error_response(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"Failed to read request body",
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -186,7 +186,27 @@ async fn proxy_api(
|
|||
}
|
||||
|
||||
match proxy_req.send().await {
|
||||
Ok(resp) => {
|
||||
Ok(resp) => build_proxy_response(resp).await,
|
||||
Err(e) => {
|
||||
error!("Proxy request failed: {e}");
|
||||
build_error_response(StatusCode::BAD_GATEWAY, &format!("Proxy error: {e}"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn build_error_response(status: StatusCode, message: &str) -> Response<Body> {
|
||||
Response::builder()
|
||||
.status(status)
|
||||
.body(Body::from(message.to_string()))
|
||||
.unwrap_or_else(|_| {
|
||||
Response::builder()
|
||||
.status(StatusCode::INTERNAL_SERVER_ERROR)
|
||||
.body(Body::from("Failed to build error response"))
|
||||
.unwrap_or_default()
|
||||
})
|
||||
}
|
||||
|
||||
async fn build_proxy_response(resp: reqwest::Response) -> Response<Body> {
|
||||
let status = resp.status();
|
||||
let headers = resp.headers().clone();
|
||||
|
||||
|
|
@ -194,32 +214,23 @@ async fn proxy_api(
|
|||
Ok(body) => {
|
||||
let mut response = Response::builder().status(status);
|
||||
|
||||
for (name, value) in headers.iter() {
|
||||
for (name, value) in &headers {
|
||||
response = response.header(name, value);
|
||||
}
|
||||
|
||||
response.body(Body::from(body)).unwrap_or_else(|_| {
|
||||
Response::builder()
|
||||
.status(StatusCode::INTERNAL_SERVER_ERROR)
|
||||
.body(Body::from("Failed to build response"))
|
||||
.unwrap()
|
||||
build_error_response(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"Failed to build response",
|
||||
)
|
||||
})
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to read response body: {}", e);
|
||||
Response::builder()
|
||||
.status(StatusCode::BAD_GATEWAY)
|
||||
.body(Body::from(format!("Failed to read response: {}", e)))
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Proxy request failed: {}", e);
|
||||
Response::builder()
|
||||
.status(StatusCode::BAD_GATEWAY)
|
||||
.body(Body::from(format!("Proxy error: {}", e)))
|
||||
.unwrap()
|
||||
error!("Failed to read response body: {e}");
|
||||
build_error_response(
|
||||
StatusCode::BAD_GATEWAY,
|
||||
&format!("Failed to read response: {e}"),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -244,6 +255,7 @@ async fn ws_proxy(
|
|||
ws.on_upgrade(move |socket| handle_ws_proxy(socket, state, params))
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
async fn handle_ws_proxy(client_socket: WebSocket, state: AppState, params: WsQuery) {
|
||||
let backend_url = format!(
|
||||
"{}/ws?session_id={}&user_id={}",
|
||||
|
|
@ -256,13 +268,16 @@ async fn handle_ws_proxy(client_socket: WebSocket, state: AppState, params: WsQu
|
|||
params.user_id
|
||||
);
|
||||
|
||||
info!("Proxying WebSocket to: {}", backend_url);
|
||||
info!("Proxying WebSocket to: {backend_url}");
|
||||
|
||||
let tls_connector = native_tls::TlsConnector::builder()
|
||||
let Ok(tls_connector) = native_tls::TlsConnector::builder()
|
||||
.danger_accept_invalid_certs(true)
|
||||
.danger_accept_invalid_hostnames(true)
|
||||
.build()
|
||||
.expect("Failed to build TLS connector");
|
||||
else {
|
||||
error!("Failed to build TLS connector");
|
||||
return;
|
||||
};
|
||||
|
||||
let connector = tokio_tungstenite::Connector::NativeTls(tls_connector);
|
||||
|
||||
|
|
@ -272,7 +287,7 @@ async fn handle_ws_proxy(client_socket: WebSocket, state: AppState, params: WsQu
|
|||
let backend_socket = match backend_result {
|
||||
Ok((socket, _)) => socket,
|
||||
Err(e) => {
|
||||
error!("Failed to connect to backend WebSocket: {}", e);
|
||||
error!("Failed to connect to backend WebSocket: {e}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
|
@ -350,14 +365,14 @@ async fn handle_ws_proxy(client_socket: WebSocket, state: AppState, params: WsQu
|
|||
}
|
||||
}
|
||||
Ok(TungsteniteMessage::Close(_)) | Err(_) => break,
|
||||
_ => {}
|
||||
Ok(_) => {}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
tokio::select! {
|
||||
_ = client_to_backend => info!("Client connection closed"),
|
||||
_ = backend_to_client => info!("Backend connection closed"),
|
||||
() = client_to_backend => info!("Client connection closed"),
|
||||
() = backend_to_client => info!("Backend connection closed"),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -373,14 +388,14 @@ fn create_ui_router() -> Router<AppState> {
|
|||
Router::new().fallback(any(proxy_api))
|
||||
}
|
||||
|
||||
fn add_static_routes(router: Router<AppState>, suite_path: &PathBuf) -> Router<AppState> {
|
||||
fn add_static_routes(router: Router<AppState>, suite_path: &Path) -> Router<AppState> {
|
||||
let mut r = router;
|
||||
|
||||
for dir in SUITE_DIRS {
|
||||
let path = suite_path.join(dir);
|
||||
r = r
|
||||
.nest_service(&format!("/suite/{}", dir), ServeDir::new(path.clone()))
|
||||
.nest_service(&format!("/{}", dir), ServeDir::new(path));
|
||||
.nest_service(&format!("/suite/{dir}"), ServeDir::new(path.clone()))
|
||||
.nest_service(&format!("/{dir}"), ServeDir::new(path));
|
||||
}
|
||||
|
||||
r
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue