diff --git a/Cargo.toml b/Cargo.toml index 764b9ec..330a0f1 100644 --- a/Cargo.toml +++ b/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" diff --git a/PROMPT.md b/PROMPT.md index 5e980ec..4e77210 100644 --- a/PROMPT.md +++ b/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 \ No newline at end of file +- **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. \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 96cad8b..b366bd8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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. diff --git a/src/main.rs b/src/main.rs index 8689ae9..09a1310 100644 --- a/src/main.rs +++ b/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..."), } } diff --git a/src/shared/mod.rs b/src/shared/mod.rs index 83687a4..dd9a1b6 100644 --- a/src/shared/mod.rs +++ b/src/shared/mod.rs @@ -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. diff --git a/src/shared/state.rs b/src/shared/state.rs index 66e6696..452d4f5 100644 --- a/src/shared/state.rs +++ b/src/shared/state.rs @@ -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, } 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 } diff --git a/src/ui_server/mod.rs b/src/ui_server/mod.rs index a0fea05..e50b179 100644 --- a/src/ui_server/mod.rs +++ b/src/ui_server/mod.rs @@ -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) -> (StatusCode, axum::Json) { - 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 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,40 +186,51 @@ async fn proxy_api( } match proxy_req.send().await { - Ok(resp) => { - let status = resp.status(); - let headers = resp.headers().clone(); + Ok(resp) => build_proxy_response(resp).await, + Err(e) => { + error!("Proxy request failed: {e}"); + build_error_response(StatusCode::BAD_GATEWAY, &format!("Proxy error: {e}")) + } + } +} - match resp.bytes().await { - Ok(body) => { - let mut response = Response::builder().status(status); +fn build_error_response(status: StatusCode, message: &str) -> Response { + 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() + }) +} - for (name, value) in headers.iter() { - response = response.header(name, value); - } +async fn build_proxy_response(resp: reqwest::Response) -> Response { + let status = resp.status(); + let headers = resp.headers().clone(); - response.body(Body::from(body)).unwrap_or_else(|_| { - Response::builder() - .status(StatusCode::INTERNAL_SERVER_ERROR) - .body(Body::from("Failed to build response")) - .unwrap() - }) - } - 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() - } + match resp.bytes().await { + Ok(body) => { + let mut response = Response::builder().status(status); + + for (name, value) in &headers { + response = response.header(name, value); } + + response.body(Body::from(body)).unwrap_or_else(|_| { + build_error_response( + StatusCode::INTERNAL_SERVER_ERROR, + "Failed to build response", + ) + }) } 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 { Router::new().fallback(any(proxy_api)) } -fn add_static_routes(router: Router, suite_path: &PathBuf) -> Router { +fn add_static_routes(router: Router, suite_path: &Path) -> Router { 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