feat(logging): Implement structured logging across the frontend
- Introduced a new `logger` module for structured logging with levels, timestamps, and styled console output. - Replaced ad-hoc console logging with structured logger in various modules including API client, WebSocket, auth, and feature components. - Enhanced logging in `app.rs`, `auth.rs`, `dashboard`, `messages`, `live`, and `polish` features. - Updated UI components to include logging for user interactions and state changes. - Rewrote `app.css` for a premium design overhaul, introducing glassmorphism, gradients, and improved responsiveness. - Added a new `plan.md` file outlining the scope and changes made in this commit.
This commit is contained in:
@@ -1,5 +1,8 @@
|
|||||||
use crate::api::client::{request, ApiError};
|
use crate::api::client::{request, ApiError};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use crate::{log_error, log_info, log_warn, make_logger};
|
||||||
|
|
||||||
|
make_logger!();
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
struct LoginPayload {
|
struct LoginPayload {
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ use serde::de::DeserializeOwned;
|
|||||||
use wasm_bindgen::prelude::*;
|
use wasm_bindgen::prelude::*;
|
||||||
use wasm_bindgen_futures::JsFuture;
|
use wasm_bindgen_futures::JsFuture;
|
||||||
use web_sys::{Headers, Request, RequestInit, RequestMode, Response};
|
use web_sys::{Headers, Request, RequestInit, RequestMode, Response};
|
||||||
|
use crate::{log_debug, log_error, make_logger};
|
||||||
|
|
||||||
|
make_logger!();
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct ApiError {
|
pub struct ApiError {
|
||||||
@@ -44,9 +47,13 @@ pub async fn request<T: DeserializeOwned>(
|
|||||||
) -> Result<T, ApiError> {
|
) -> Result<T, ApiError> {
|
||||||
let url = format!("{}{}", get_base_url(), path);
|
let url = format!("{}{}", get_base_url(), path);
|
||||||
|
|
||||||
let headers = Headers::new().map_err(|_| ApiError {
|
let headers = Headers::new().map_err(|_| {
|
||||||
message: "Failed to create headers".to_string(),
|
let msg = "Failed to create headers";
|
||||||
status_code: 0,
|
log_error!("API {} {} failed: {}", method, path, msg);
|
||||||
|
ApiError {
|
||||||
|
message: msg.to_string(),
|
||||||
|
status_code: 0,
|
||||||
|
}
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
if let Some(password) = get_auth_header() {
|
if let Some(password) = get_auth_header() {
|
||||||
@@ -57,6 +64,8 @@ pub async fn request<T: DeserializeOwned>(
|
|||||||
headers.set("Content-Type", "application/json").ok();
|
headers.set("Content-Type", "application/json").ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log_debug!("{} {} ->", method, path);
|
||||||
|
|
||||||
let opts = RequestInit::new();
|
let opts = RequestInit::new();
|
||||||
opts.set_method(method);
|
opts.set_method(method);
|
||||||
opts.set_headers(&headers);
|
opts.set_headers(&headers);
|
||||||
@@ -66,67 +75,106 @@ pub async fn request<T: DeserializeOwned>(
|
|||||||
opts.set_body(&JsValue::from_str(json_body));
|
opts.set_body(&JsValue::from_str(json_body));
|
||||||
}
|
}
|
||||||
|
|
||||||
let request = Request::new_with_str_and_init(&url, &opts).map_err(|e| ApiError {
|
let request = Request::new_with_str_and_init(&url, &opts).map_err(|e| {
|
||||||
message: format!("Failed to create request: {:?}", e),
|
let msg = format!("Failed to create request: {:?}", e);
|
||||||
status_code: 0,
|
log_error!("API {} {} failed: {}", method, path, msg);
|
||||||
|
ApiError {
|
||||||
|
message: msg,
|
||||||
|
status_code: 0,
|
||||||
|
}
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let window = web_sys::window().ok_or(ApiError {
|
let window = web_sys::window().ok_or_else(|| {
|
||||||
message: "No window".to_string(),
|
let msg = "No window";
|
||||||
status_code: 0,
|
log_error!("API {} {} failed: {}", method, path, msg);
|
||||||
|
ApiError {
|
||||||
|
message: msg.to_string(),
|
||||||
|
status_code: 0,
|
||||||
|
}
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let resp_value = JsFuture::from(window.fetch_with_request(&request))
|
let resp_value = JsFuture::from(window.fetch_with_request(&request))
|
||||||
.await
|
.await
|
||||||
.map_err(|e| ApiError {
|
.map_err(|e| {
|
||||||
message: format!("Fetch failed: {:?}", e),
|
let msg = format!("Fetch failed: {:?}", e);
|
||||||
status_code: 0,
|
log_error!("API {} {} failed: {}", method, path, msg);
|
||||||
|
ApiError {
|
||||||
|
message: msg,
|
||||||
|
status_code: 0,
|
||||||
|
}
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let response: Response = resp_value.dyn_into().map_err(|_| ApiError {
|
let response: Response = resp_value.dyn_into().map_err(|_| {
|
||||||
message: "Invalid response".to_string(),
|
let msg = "Invalid response";
|
||||||
status_code: 0,
|
log_error!("API {} {} failed: {}", method, path, msg);
|
||||||
|
ApiError {
|
||||||
|
message: msg.to_string(),
|
||||||
|
status_code: 0,
|
||||||
|
}
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let status = response.status();
|
let status = response.status();
|
||||||
if status >= 400 {
|
if status >= 400 {
|
||||||
let text = JsFuture::from(response.text().map_err(|_| ApiError {
|
let text = JsFuture::from(response.text().map_err(|_| {
|
||||||
message: "Failed to read error body".to_string(),
|
let msg = "Failed to read error body";
|
||||||
status_code: status,
|
log_error!("API {} {} failed: {}", method, path, msg);
|
||||||
|
ApiError {
|
||||||
|
message: msg.to_string(),
|
||||||
|
status_code: status,
|
||||||
|
}
|
||||||
})?)
|
})?)
|
||||||
.await
|
.await
|
||||||
.ok()
|
.ok()
|
||||||
.and_then(|v| v.as_string())
|
.and_then(|v| v.as_string())
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
log_error!("API {} {} failed: status={} {}", method, path, status, text);
|
||||||
return Err(ApiError {
|
return Err(ApiError {
|
||||||
message: text,
|
message: text,
|
||||||
status_code: status,
|
status_code: status,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let text = JsFuture::from(response.text().map_err(|_| ApiError {
|
let text = JsFuture::from(response.text().map_err(|_| {
|
||||||
message: "Failed to read response body".to_string(),
|
let msg = "Failed to read response body";
|
||||||
status_code: status,
|
log_error!("API {} {} failed: {}", method, path, msg);
|
||||||
|
ApiError {
|
||||||
|
message: msg.to_string(),
|
||||||
|
status_code: status,
|
||||||
|
}
|
||||||
})?)
|
})?)
|
||||||
.await
|
.await
|
||||||
.map_err(|_| ApiError {
|
.map_err(|_| {
|
||||||
message: "Failed to await response".to_string(),
|
let msg = "Failed to await response";
|
||||||
status_code: status,
|
log_error!("API {} {} failed: {}", method, path, msg);
|
||||||
|
ApiError {
|
||||||
|
message: msg.to_string(),
|
||||||
|
status_code: status,
|
||||||
|
}
|
||||||
})?
|
})?
|
||||||
.as_string()
|
.as_string()
|
||||||
.ok_or(ApiError {
|
.ok_or_else(|| {
|
||||||
message: "Response is not text".to_string(),
|
let msg = "Response is not text";
|
||||||
status_code: status,
|
log_error!("API {} {} failed: {}", method, path, msg);
|
||||||
|
ApiError {
|
||||||
|
message: msg.to_string(),
|
||||||
|
status_code: status,
|
||||||
|
}
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
serde_json::from_str(&text).map_err(|e| ApiError {
|
log_debug!("{} {} <- {}", method, path, status);
|
||||||
message: format!(
|
|
||||||
|
serde_json::from_str(&text).map_err(|e| {
|
||||||
|
let msg = format!(
|
||||||
"JSON parse error: {} — body: {}",
|
"JSON parse error: {} — body: {}",
|
||||||
e,
|
e,
|
||||||
&text[..text.len().min(200)]
|
&text[..text.len().min(200)]
|
||||||
),
|
);
|
||||||
status_code: status,
|
log_error!("API {} {} failed: {}", method, path, msg);
|
||||||
|
ApiError {
|
||||||
|
message: msg,
|
||||||
|
status_code: status,
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
use crate::api::client::{request, ApiError};
|
use crate::api::client::{request, ApiError};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct AppConfigResponse {
|
pub struct AppConfigResponse {
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
use crate::api::client::{request, ApiError};
|
use crate::api::client::{request, ApiError};
|
||||||
use shared_types::dashboard::*;
|
use shared_types::dashboard::*;
|
||||||
|
use crate::{log_debug, make_logger};
|
||||||
|
|
||||||
|
make_logger!();
|
||||||
|
|
||||||
/// GET /api/dashboard/stats
|
/// GET /api/dashboard/stats
|
||||||
pub async fn get_dashboard_stats() -> Result<DashboardStats, ApiError> {
|
pub async fn get_dashboard_stats() -> Result<DashboardStats, ApiError> {
|
||||||
|
log_debug!("get_dashboard_stats");
|
||||||
request("GET", "/api/dashboard/stats", None).await
|
request("GET", "/api/dashboard/stats", None).await
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -12,6 +16,7 @@ pub async fn get_dashboard_users(
|
|||||||
cursor: Option<&str>,
|
cursor: Option<&str>,
|
||||||
search: Option<&str>,
|
search: Option<&str>,
|
||||||
) -> Result<PaginatedUsers, ApiError> {
|
) -> Result<PaginatedUsers, ApiError> {
|
||||||
|
log_debug!("get_dashboard_users: limit={:?}, cursor={:?}, search={:?}", limit, cursor, search);
|
||||||
let mut path = "/api/dashboard/users".to_string();
|
let mut path = "/api/dashboard/users".to_string();
|
||||||
let mut params = vec![];
|
let mut params = vec![];
|
||||||
if let Some(l) = limit {
|
if let Some(l) = limit {
|
||||||
@@ -38,6 +43,7 @@ pub struct PaginatedUsers {
|
|||||||
|
|
||||||
/// GET /api/dashboard/users/{userId}
|
/// GET /api/dashboard/users/{userId}
|
||||||
pub async fn get_dashboard_user_detail(user_id: &str) -> Result<DashboardUserDetail, ApiError> {
|
pub async fn get_dashboard_user_detail(user_id: &str) -> Result<DashboardUserDetail, ApiError> {
|
||||||
|
log_debug!("get_dashboard_user_detail: user_id={}", user_id);
|
||||||
request("GET", &format!("/api/dashboard/users/{}", user_id), None).await
|
request("GET", &format!("/api/dashboard/users/{}", user_id), None).await
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,6 +54,7 @@ pub async fn get_dashboard_channels(
|
|||||||
search: Option<&str>,
|
search: Option<&str>,
|
||||||
guild_id: Option<&str>,
|
guild_id: Option<&str>,
|
||||||
) -> Result<PaginatedChannels, ApiError> {
|
) -> Result<PaginatedChannels, ApiError> {
|
||||||
|
log_debug!("get_dashboard_channels: limit={:?}, cursor={:?}, search={:?}, guild_id={:?}", limit, cursor, search, guild_id);
|
||||||
let mut path = "/api/dashboard/channels".to_string();
|
let mut path = "/api/dashboard/channels".to_string();
|
||||||
let mut params = vec![];
|
let mut params = vec![];
|
||||||
if let Some(l) = limit {
|
if let Some(l) = limit {
|
||||||
@@ -79,6 +86,7 @@ pub struct PaginatedChannels {
|
|||||||
pub async fn get_dashboard_channel_detail(
|
pub async fn get_dashboard_channel_detail(
|
||||||
channel_id: &str,
|
channel_id: &str,
|
||||||
) -> Result<DashboardChannelDetail, ApiError> {
|
) -> Result<DashboardChannelDetail, ApiError> {
|
||||||
|
log_debug!("get_dashboard_channel_detail: channel_id={}", channel_id);
|
||||||
request(
|
request(
|
||||||
"GET",
|
"GET",
|
||||||
&format!("/api/dashboard/channels/{}", channel_id),
|
&format!("/api/dashboard/channels/{}", channel_id),
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
use crate::api::client::{request, ApiError};
|
use crate::api::client::{request, ApiError};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
struct MascotChatRequest<'a> {
|
struct MascotChatRequest<'a> {
|
||||||
message: &'a str,
|
message: &'a str,
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
use crate::api::client::{request, ApiError};
|
use crate::api::client::{request, ApiError};
|
||||||
use shared_types::message::{MessageRecord, PageResult};
|
use shared_types::message::{MessageRecord, PageResult};
|
||||||
|
use crate::{log_debug, make_logger};
|
||||||
|
|
||||||
|
make_logger!();
|
||||||
|
|
||||||
/// GET /api/messages?guildId=&limit=&channelId=&cursor=
|
/// GET /api/messages?guildId=&limit=&channelId=&cursor=
|
||||||
pub async fn get_messages(
|
pub async fn get_messages(
|
||||||
@@ -8,6 +11,7 @@ pub async fn get_messages(
|
|||||||
channel_id: Option<&str>,
|
channel_id: Option<&str>,
|
||||||
cursor: Option<&str>,
|
cursor: Option<&str>,
|
||||||
) -> Result<PageResult<MessageRecord>, ApiError> {
|
) -> Result<PageResult<MessageRecord>, ApiError> {
|
||||||
|
log_debug!("get_messages: guild_id={}, limit={:?}, channel_id={:?}, cursor={:?}", guild_id, limit, channel_id, cursor);
|
||||||
let mut path = format!("/api/messages?guildId={}", guild_id);
|
let mut path = format!("/api/messages?guildId={}", guild_id);
|
||||||
if let Some(l) = limit {
|
if let Some(l) = limit {
|
||||||
path.push_str(&format!("&limit={}", l));
|
path.push_str(&format!("&limit={}", l));
|
||||||
@@ -27,6 +31,7 @@ pub async fn get_review_messages(
|
|||||||
limit: Option<u32>,
|
limit: Option<u32>,
|
||||||
channel_id: Option<&str>,
|
channel_id: Option<&str>,
|
||||||
) -> Result<PageResult<MessageRecord>, ApiError> {
|
) -> Result<PageResult<MessageRecord>, ApiError> {
|
||||||
|
log_debug!("get_review_messages: limit={:?}, channel_id={:?}", limit, channel_id);
|
||||||
let mut path = "/api/review".to_string();
|
let mut path = "/api/review".to_string();
|
||||||
let mut params = vec![];
|
let mut params = vec![];
|
||||||
if let Some(l) = limit {
|
if let Some(l) = limit {
|
||||||
@@ -46,6 +51,7 @@ pub async fn get_images(
|
|||||||
guild_id: &str,
|
guild_id: &str,
|
||||||
limit: Option<u32>,
|
limit: Option<u32>,
|
||||||
) -> Result<PageResult<MessageRecord>, ApiError> {
|
) -> Result<PageResult<MessageRecord>, ApiError> {
|
||||||
|
log_debug!("get_images: guild_id={}, limit={:?}", guild_id, limit);
|
||||||
let mut path = format!("/api/messages/images?guildId={}", guild_id);
|
let mut path = format!("/api/messages/images?guildId={}", guild_id);
|
||||||
if let Some(l) = limit {
|
if let Some(l) = limit {
|
||||||
path.push_str(&format!("&limit={}", l));
|
path.push_str(&format!("&limit={}", l));
|
||||||
@@ -55,11 +61,13 @@ pub async fn get_images(
|
|||||||
|
|
||||||
/// GET /api/messages/detail/{id}
|
/// GET /api/messages/detail/{id}
|
||||||
pub async fn get_message_detail(id: &str) -> Result<Option<MessageRecord>, ApiError> {
|
pub async fn get_message_detail(id: &str) -> Result<Option<MessageRecord>, ApiError> {
|
||||||
|
log_debug!("get_message_detail: id={}", id);
|
||||||
request("GET", &format!("/api/messages/detail/{}", id), None).await
|
request("GET", &format!("/api/messages/detail/{}", id), None).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// POST /api/messages/{id}/reanalyze
|
/// POST /api/messages/{id}/reanalyze
|
||||||
pub async fn reanalyze_message(id: &str) -> Result<(), ApiError> {
|
pub async fn reanalyze_message(id: &str) -> Result<(), ApiError> {
|
||||||
|
log_debug!("reanalyze_message: id={}", id);
|
||||||
let _: serde_json::Value = request(
|
let _: serde_json::Value = request(
|
||||||
"POST",
|
"POST",
|
||||||
&format!("/api/messages/{}/reanalyze", id),
|
&format!("/api/messages/{}/reanalyze", id),
|
||||||
@@ -71,6 +79,7 @@ pub async fn reanalyze_message(id: &str) -> Result<(), ApiError> {
|
|||||||
|
|
||||||
/// POST /api/messages/reanalyze-batch
|
/// POST /api/messages/reanalyze-batch
|
||||||
pub async fn reanalyze_batch() -> Result<u64, ApiError> {
|
pub async fn reanalyze_batch() -> Result<u64, ApiError> {
|
||||||
|
log_debug!("reanalyze_batch");
|
||||||
#[derive(serde::Deserialize)]
|
#[derive(serde::Deserialize)]
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
struct BatchResp {
|
struct BatchResp {
|
||||||
@@ -86,6 +95,7 @@ pub async fn search_messages(
|
|||||||
query: &str,
|
query: &str,
|
||||||
limit: Option<u32>,
|
limit: Option<u32>,
|
||||||
) -> Result<Vec<MessageRecord>, ApiError> {
|
) -> Result<Vec<MessageRecord>, ApiError> {
|
||||||
|
log_debug!("search_messages: query={}, limit={:?}", query, limit);
|
||||||
#[derive(serde::Deserialize)]
|
#[derive(serde::Deserialize)]
|
||||||
struct SearchResult {
|
struct SearchResult {
|
||||||
results: Vec<MessageRecord>,
|
results: Vec<MessageRecord>,
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
use crate::api::client::{request, request_no_body, ApiError};
|
use crate::api::client::{request, request_no_body, ApiError};
|
||||||
use shared_types::recording::VoiceRecordingListResponse;
|
use shared_types::recording::VoiceRecordingListResponse;
|
||||||
|
|
||||||
/// GET /api/recordings?limit=&cursor=
|
/// GET /api/recordings?limit=&cursor=
|
||||||
pub async fn get_recordings(
|
pub async fn get_recordings(
|
||||||
limit: Option<u32>,
|
limit: Option<u32>,
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ use serde::Serialize;
|
|||||||
use shared_types::guild::{Channel, Guild};
|
use shared_types::guild::{Channel, Guild};
|
||||||
use shared_types::media::MediaState;
|
use shared_types::media::MediaState;
|
||||||
use shared_types::voice::VoiceStatus;
|
use shared_types::voice::VoiceStatus;
|
||||||
|
|
||||||
/// GET /api/guilds
|
/// GET /api/guilds
|
||||||
pub async fn get_guilds() -> Result<Vec<Guild>, ApiError> {
|
pub async fn get_guilds() -> Result<Vec<Guild>, ApiError> {
|
||||||
request("GET", "/api/guilds", None).await
|
request("GET", "/api/guilds", None).await
|
||||||
|
|||||||
+343
-1326
File diff suppressed because it is too large
Load Diff
@@ -8,6 +8,9 @@ use crate::ws::context::WsContext;
|
|||||||
use leptos::prelude::*;
|
use leptos::prelude::*;
|
||||||
use shared_types::ui_state::Tab;
|
use shared_types::ui_state::Tab;
|
||||||
use wasm_bindgen_futures::spawn_local;
|
use wasm_bindgen_futures::spawn_local;
|
||||||
|
use crate::{log_info, log_warn, make_logger};
|
||||||
|
|
||||||
|
make_logger!();
|
||||||
|
|
||||||
/// Derive WebSocket URL from the page's own origin.
|
/// Derive WebSocket URL from the page's own origin.
|
||||||
/// In development (serve on :8080, backend on :3001) use the detected host + /ws path.
|
/// In development (serve on :8080, backend on :3001) use the detected host + /ws path.
|
||||||
@@ -77,6 +80,7 @@ pub fn App() -> impl IntoView {
|
|||||||
provide_context(ws.clone());
|
provide_context(ws.clone());
|
||||||
|
|
||||||
ws.connect();
|
ws.connect();
|
||||||
|
log_info!("App mounted, WS connecting to {}", get_ws_url());
|
||||||
|
|
||||||
// Try to fetch config on startup (works if password is already in localStorage)
|
// Try to fetch config on startup (works if password is already in localStorage)
|
||||||
spawn_local({
|
spawn_local({
|
||||||
@@ -84,16 +88,11 @@ pub fn App() -> impl IntoView {
|
|||||||
async move {
|
async move {
|
||||||
match config_api::get_config().await {
|
match config_api::get_config().await {
|
||||||
Ok(cfg) => {
|
Ok(cfg) => {
|
||||||
web_sys::console::log_2(
|
log_info!("[config] fetched OK — monitorGuildId={:?}", cfg.monitor_guild_id);
|
||||||
&"[config] fetched OK".into(),
|
|
||||||
&format!("monitorGuildId={:?}", cfg.monitor_guild_id).into(),
|
|
||||||
);
|
|
||||||
config.monitor_guild_id.set(cfg.monitor_guild_id);
|
config.monitor_guild_id.set(cfg.monitor_guild_id);
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
web_sys::console::log_1(
|
log_warn!("[config] failed to fetch: {}", e);
|
||||||
&format!("[config] failed to fetch: {}", e).into(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -110,9 +109,7 @@ pub fn App() -> impl IntoView {
|
|||||||
config.monitor_guild_id.set(cfg.monitor_guild_id);
|
config.monitor_guild_id.set(cfg.monitor_guild_id);
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
web_sys::console::log_1(
|
log_info!("[config] fetch after auth failed: {}", e);
|
||||||
&format!("[config] fetch after auth failed: {}", e).into(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ use crate::app::UiContext;
|
|||||||
use leptos::prelude::*;
|
use leptos::prelude::*;
|
||||||
use shared_types::ui_state::Tab;
|
use shared_types::ui_state::Tab;
|
||||||
use wasm_bindgen_futures::spawn_local;
|
use wasm_bindgen_futures::spawn_local;
|
||||||
|
use crate::{log_error, log_info, log_warn, make_logger};
|
||||||
|
|
||||||
|
make_logger!();
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn AuthOverlay() -> impl IntoView {
|
pub fn AuthOverlay() -> impl IntoView {
|
||||||
@@ -32,6 +35,7 @@ pub fn AuthOverlay() -> impl IntoView {
|
|||||||
spawn_local(async move {
|
spawn_local(async move {
|
||||||
match auth_api::login(&pwd_clone).await {
|
match auth_api::login(&pwd_clone).await {
|
||||||
Ok(true) => {
|
Ok(true) => {
|
||||||
|
log_info!("Auth login successful");
|
||||||
// Store password in sessionStorage
|
// Store password in sessionStorage
|
||||||
if let Some(storage) = web_sys::window()
|
if let Some(storage) = web_sys::window()
|
||||||
.and_then(|w| w.local_storage().ok())
|
.and_then(|w| w.local_storage().ok())
|
||||||
@@ -43,9 +47,11 @@ pub fn AuthOverlay() -> impl IntoView {
|
|||||||
auth_clone.password.set(pwd_clone);
|
auth_clone.password.set(pwd_clone);
|
||||||
}
|
}
|
||||||
Ok(false) => {
|
Ok(false) => {
|
||||||
|
log_warn!("Auth login failed - wrong password");
|
||||||
set_error_clone.set(Some("Login gagal — password salah".to_string()));
|
set_error_clone.set(Some("Login gagal — password salah".to_string()));
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
log_error!("Auth login error: {}", e.message);
|
||||||
set_error_clone.set(Some(format!("Error: {}", e.message)));
|
set_error_clone.set(Some(format!("Error: {}", e.message)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
use leptos::prelude::*;
|
use leptos::prelude::*;
|
||||||
use shared_types::dashboard::{DashboardStats, TopChannel};
|
use shared_types::dashboard::{DashboardStats, TopChannel};
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn StatsOverview(
|
pub fn StatsOverview(
|
||||||
stats: Option<DashboardStats>,
|
stats: Option<DashboardStats>,
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ use leptos::prelude::*;
|
|||||||
use shared_types::dashboard::{DashboardChannel, DashboardStats, DashboardUser};
|
use shared_types::dashboard::{DashboardChannel, DashboardStats, DashboardUser};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use wasm_bindgen_futures::spawn_local;
|
use wasm_bindgen_futures::spawn_local;
|
||||||
|
use crate::{log_error, log_info, make_logger};
|
||||||
|
|
||||||
|
make_logger!();
|
||||||
|
|
||||||
#[derive(Clone, PartialEq)]
|
#[derive(Clone, PartialEq)]
|
||||||
enum DashboardTab {
|
enum DashboardTab {
|
||||||
@@ -36,10 +39,17 @@ pub fn DashboardPanel() -> impl IntoView {
|
|||||||
let fetch_stats: Arc<dyn Fn() + Send + Sync + 'static> = Arc::new(move || {
|
let fetch_stats: Arc<dyn Fn() + Send + Sync + 'static> = Arc::new(move || {
|
||||||
stats_loading.set(true);
|
stats_loading.set(true);
|
||||||
stats_error.set(None);
|
stats_error.set(None);
|
||||||
|
log_info!("Dashboard fetching stats...");
|
||||||
spawn_local(async move {
|
spawn_local(async move {
|
||||||
match crate::api::dashboard::get_dashboard_stats().await {
|
match crate::api::dashboard::get_dashboard_stats().await {
|
||||||
Ok(data) => stats.set(Some(data)),
|
Ok(data) => {
|
||||||
Err(err) => stats_error.set(Some(format!("Failed to load stats: {}", err))),
|
log_info!("Dashboard stats loaded: {} messages", data.total_messages);
|
||||||
|
stats.set(Some(data));
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
log_error!("Dashboard stats error: {}", err);
|
||||||
|
stats_error.set(Some(format!("Failed to load stats: {}", err)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
stats_loading.set(false);
|
stats_loading.set(false);
|
||||||
});
|
});
|
||||||
@@ -51,6 +61,7 @@ pub fn DashboardPanel() -> impl IntoView {
|
|||||||
}
|
}
|
||||||
users_loading.set(true);
|
users_loading.set(true);
|
||||||
users_error.set(None);
|
users_error.set(None);
|
||||||
|
log_info!("Dashboard fetching users...");
|
||||||
|
|
||||||
let cursor = if reset { None } else { users_cursor.get() };
|
let cursor = if reset { None } else { users_cursor.get() };
|
||||||
let search = users_search.get();
|
let search = users_search.get();
|
||||||
@@ -64,6 +75,7 @@ pub fn DashboardPanel() -> impl IntoView {
|
|||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(page) => {
|
Ok(page) => {
|
||||||
|
log_info!("Dashboard users loaded: {} users", page.data.len());
|
||||||
if reset {
|
if reset {
|
||||||
users.set(page.data);
|
users.set(page.data);
|
||||||
} else {
|
} else {
|
||||||
@@ -73,7 +85,10 @@ pub fn DashboardPanel() -> impl IntoView {
|
|||||||
}
|
}
|
||||||
users_cursor.set(page.next_cursor);
|
users_cursor.set(page.next_cursor);
|
||||||
}
|
}
|
||||||
Err(err) => users_error.set(Some(format!("Failed to load users: {}", err))),
|
Err(err) => {
|
||||||
|
log_error!("Dashboard users error: {}", err);
|
||||||
|
users_error.set(Some(format!("Failed to load users: {}", err)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
users_loading.set(false);
|
users_loading.set(false);
|
||||||
});
|
});
|
||||||
@@ -85,6 +100,7 @@ pub fn DashboardPanel() -> impl IntoView {
|
|||||||
}
|
}
|
||||||
channels_loading.set(true);
|
channels_loading.set(true);
|
||||||
channels_error.set(None);
|
channels_error.set(None);
|
||||||
|
log_info!("Dashboard fetching channels...");
|
||||||
|
|
||||||
let cursor = if reset { None } else { channels_cursor.get() };
|
let cursor = if reset { None } else { channels_cursor.get() };
|
||||||
let search = channels_search.get();
|
let search = channels_search.get();
|
||||||
@@ -99,6 +115,7 @@ pub fn DashboardPanel() -> impl IntoView {
|
|||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(page) => {
|
Ok(page) => {
|
||||||
|
log_info!("Dashboard channels loaded: {} channels", page.data.len());
|
||||||
if reset {
|
if reset {
|
||||||
channels.set(page.data);
|
channels.set(page.data);
|
||||||
} else {
|
} else {
|
||||||
@@ -108,7 +125,10 @@ pub fn DashboardPanel() -> impl IntoView {
|
|||||||
}
|
}
|
||||||
channels_cursor.set(page.next_cursor);
|
channels_cursor.set(page.next_cursor);
|
||||||
}
|
}
|
||||||
Err(err) => channels_error.set(Some(format!("Failed to load channels: {}", err))),
|
Err(err) => {
|
||||||
|
log_error!("Dashboard channels error: {}", err);
|
||||||
|
channels_error.set(Some(format!("Failed to load channels: {}", err)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
channels_loading.set(false);
|
channels_loading.set(false);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ use components::{
|
|||||||
use leptos::prelude::*;
|
use leptos::prelude::*;
|
||||||
use shared_types::media::MediaState;
|
use shared_types::media::MediaState;
|
||||||
use shared_types::voice::ActiveSpeaker;
|
use shared_types::voice::ActiveSpeaker;
|
||||||
|
use crate::{log_debug, log_info, make_logger};
|
||||||
|
|
||||||
|
make_logger!();
|
||||||
|
|
||||||
/// LivePanel — Composition shell for all voice and media components.
|
/// LivePanel — Composition shell for all voice and media components.
|
||||||
/// Shows an auth overlay if not authenticated, otherwise shows voice controls.
|
/// Shows an auth overlay if not authenticated, otherwise shows voice controls.
|
||||||
@@ -28,10 +31,12 @@ pub fn LivePanel() -> impl IntoView {
|
|||||||
|
|
||||||
// ── Wire WS events (runs on mount, persists while LivePanel is active) ──
|
// ── Wire WS events (runs on mount, persists while LivePanel is active) ──
|
||||||
if let Some(ref ws) = ws {
|
if let Some(ref ws) = ws {
|
||||||
|
log_info!("LivePanel wiring WS handlers");
|
||||||
// Voice active user — update speakers list
|
// Voice active user — update speakers list
|
||||||
*ws.on_voice_active_user.borrow_mut() = Some(Box::new({
|
*ws.on_voice_active_user.borrow_mut() = Some(Box::new({
|
||||||
let speakers = speakers.clone();
|
let speakers = speakers.clone();
|
||||||
move |speaker: ActiveSpeaker| {
|
move |speaker: ActiveSpeaker| {
|
||||||
|
log_debug!("LivePanel voice_active_user: {}", speaker.user_id);
|
||||||
speakers.update(|list| {
|
speakers.update(|list| {
|
||||||
if let Some(pos) = list.iter().position(|s| s.user_id == speaker.user_id) {
|
if let Some(pos) = list.iter().position(|s| s.user_id == speaker.user_id) {
|
||||||
list[pos] = speaker;
|
list[pos] = speaker;
|
||||||
@@ -46,6 +51,7 @@ pub fn LivePanel() -> impl IntoView {
|
|||||||
*ws.on_media_state.borrow_mut() = Some(Box::new({
|
*ws.on_media_state.borrow_mut() = Some(Box::new({
|
||||||
let ms = media_state.clone();
|
let ms = media_state.clone();
|
||||||
move |state: MediaState| {
|
move |state: MediaState| {
|
||||||
|
log_debug!("LivePanel media_state received");
|
||||||
ms.set(Some(state));
|
ms.set(Some(state));
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
@@ -54,6 +60,7 @@ pub fn LivePanel() -> impl IntoView {
|
|||||||
*ws.on_voice_recording_uploaded.borrow_mut() = Some(Box::new({
|
*ws.on_voice_recording_uploaded.borrow_mut() = Some(Box::new({
|
||||||
let set_refresh = set_recordings_refresh;
|
let set_refresh = set_recordings_refresh;
|
||||||
move |_recording| {
|
move |_recording| {
|
||||||
|
log_debug!("LivePanel recording_uploaded received");
|
||||||
set_refresh.update(|v| *v = v.wrapping_add(1));
|
set_refresh.update(|v| *v = v.wrapping_add(1));
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
@@ -62,6 +69,7 @@ pub fn LivePanel() -> impl IntoView {
|
|||||||
*ws.on_binary.borrow_mut() = Some(Box::new({
|
*ws.on_binary.borrow_mut() = Some(Box::new({
|
||||||
let playback = audio_playback.clone();
|
let playback = audio_playback.clone();
|
||||||
move |data: Vec<u8>| {
|
move |data: Vec<u8>| {
|
||||||
|
log_debug!("LivePanel binary PCM data received: {} bytes", data.len());
|
||||||
hooks::use_audio_playback::process_pcm_data(&playback, data);
|
hooks::use_audio_playback::process_pcm_data(&playback, data);
|
||||||
// Auto-start playback on first PCM data
|
// Auto-start playback on first PCM data
|
||||||
if !playback.active.get_untracked() {
|
if !playback.active.get_untracked() {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ use shared_types::message::{AiSeverity, AiStatus, AttachmentRef, MessageRecord};
|
|||||||
use std::sync::{Arc, OnceLock};
|
use std::sync::{Arc, OnceLock};
|
||||||
use wasm_bindgen::prelude::*;
|
use wasm_bindgen::prelude::*;
|
||||||
|
|
||||||
|
|
||||||
// ─── Helpers ──────────────────────────────────────────────
|
// ─── Helpers ──────────────────────────────────────────────
|
||||||
|
|
||||||
fn custom_emoji_regex() -> &'static Regex {
|
fn custom_emoji_regex() -> &'static Regex {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ use wasm_bindgen::prelude::*;
|
|||||||
use wasm_bindgen::JsCast;
|
use wasm_bindgen::JsCast;
|
||||||
use web_sys::IntersectionObserver;
|
use web_sys::IntersectionObserver;
|
||||||
|
|
||||||
|
|
||||||
const GROUP_WINDOW_MS: i64 = 5 * 60 * 1000;
|
const GROUP_WINDOW_MS: i64 = 5 * 60 * 1000;
|
||||||
|
|
||||||
fn group_messages(messages: Vec<MessageRecord>) -> Vec<Vec<MessageRecord>> {
|
fn group_messages(messages: Vec<MessageRecord>) -> Vec<Vec<MessageRecord>> {
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ use shared_types::message::{MessageRecord, PageResult};
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use wasm_bindgen_futures::spawn_local;
|
use wasm_bindgen_futures::spawn_local;
|
||||||
|
use crate::{log_debug, log_error, log_info, log_trace, log_warn, make_logger};
|
||||||
|
|
||||||
|
make_logger!();
|
||||||
|
|
||||||
/// Merges current messages with incoming messages, deduplicating by ID and sorting
|
/// Merges current messages with incoming messages, deduplicating by ID and sorting
|
||||||
pub fn merge_messages(current: &[MessageRecord], incoming: &[MessageRecord]) -> Vec<MessageRecord> {
|
pub fn merge_messages(current: &[MessageRecord], incoming: &[MessageRecord]) -> Vec<MessageRecord> {
|
||||||
@@ -77,9 +80,11 @@ pub fn use_messages() -> MessagesState {
|
|||||||
async move {
|
async move {
|
||||||
error_signal.set(None);
|
error_signal.set(None);
|
||||||
set_loading.set(true);
|
set_loading.set(true);
|
||||||
|
log_info!("Messages fetch start for guild {}", guild_id);
|
||||||
|
|
||||||
match get_messages(&guild_id, Some(30), None, None).await {
|
match get_messages(&guild_id, Some(30), None, None).await {
|
||||||
Ok(PageResult { data, next_cursor }) => {
|
Ok(PageResult { data, next_cursor }) => {
|
||||||
|
log_info!("Messages fetch OK: count={}, cursor={:?}", data.len(), next_cursor);
|
||||||
web_sys::console::log_3(
|
web_sys::console::log_3(
|
||||||
&"[messages] fetch OK".into(),
|
&"[messages] fetch OK".into(),
|
||||||
&format!("count={}", data.len()).into(),
|
&format!("count={}", data.len()).into(),
|
||||||
@@ -91,6 +96,7 @@ pub fn use_messages() -> MessagesState {
|
|||||||
set_loading.set(false);
|
set_loading.set(false);
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
log_warn!("Messages fetch error: {}", e);
|
||||||
web_sys::console::log_2(
|
web_sys::console::log_2(
|
||||||
&"[messages] fetch ERROR".into(),
|
&"[messages] fetch ERROR".into(),
|
||||||
&format!("{}", e).into(),
|
&format!("{}", e).into(),
|
||||||
@@ -124,15 +130,18 @@ pub fn use_messages() -> MessagesState {
|
|||||||
|
|
||||||
loading_more_signal.set(true);
|
loading_more_signal.set(true);
|
||||||
error_signal.set(None);
|
error_signal.set(None);
|
||||||
|
log_info!("Messages load more for guild {}", guild_id);
|
||||||
|
|
||||||
match get_messages(&guild_id, Some(30), None, Some(&cursor)).await {
|
match get_messages(&guild_id, Some(30), None, Some(&cursor)).await {
|
||||||
Ok(PageResult { data, next_cursor }) => {
|
Ok(PageResult { data, next_cursor }) => {
|
||||||
|
log_info!("Messages load more OK: count={}, cursor={:?}", data.len(), next_cursor);
|
||||||
let current = messages_signal.get();
|
let current = messages_signal.get();
|
||||||
messages_signal.set(merge_messages(¤t, &data));
|
messages_signal.set(merge_messages(¤t, &data));
|
||||||
cursor_signal.set(next_cursor);
|
cursor_signal.set(next_cursor);
|
||||||
loading_more_signal.set(false);
|
loading_more_signal.set(false);
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
log_warn!("Messages load more error: {}", e);
|
||||||
error_signal.set(Some(format!("Failed to load more: {}", e)));
|
error_signal.set(Some(format!("Failed to load more: {}", e)));
|
||||||
loading_more_signal.set(false);
|
loading_more_signal.set(false);
|
||||||
}
|
}
|
||||||
@@ -145,6 +154,7 @@ pub fn use_messages() -> MessagesState {
|
|||||||
spawn_local({
|
spawn_local({
|
||||||
let message_id = message_id.clone();
|
let message_id = message_id.clone();
|
||||||
async move {
|
async move {
|
||||||
|
log_info!("Messages reanalyze start for message {}", message_id);
|
||||||
// Optimistic: flip status to Processing
|
// Optimistic: flip status to Processing
|
||||||
let mut msgs = messages_signal.get();
|
let mut msgs = messages_signal.get();
|
||||||
if let Some(pos) = msgs.iter().position(|m| m.id == message_id) {
|
if let Some(pos) = msgs.iter().position(|m| m.id == message_id) {
|
||||||
@@ -157,9 +167,11 @@ pub fn use_messages() -> MessagesState {
|
|||||||
// Call API
|
// Call API
|
||||||
match reanalyze_message(&message_id).await {
|
match reanalyze_message(&message_id).await {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
|
log_info!("Messages reanalyze OK for message {}", message_id);
|
||||||
// Success: keep the Processing status (will be updated via WS)
|
// Success: keep the Processing status (will be updated via WS)
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
log_warn!("Messages reanalyze error for message {}: {}", message_id, e);
|
||||||
// Revert to Error status on failure
|
// Revert to Error status on failure
|
||||||
let mut msgs = messages_signal.get();
|
let mut msgs = messages_signal.get();
|
||||||
if let Some(pos) = msgs.iter().position(|m| m.id == message_id) {
|
if let Some(pos) = msgs.iter().position(|m| m.id == message_id) {
|
||||||
@@ -179,8 +191,10 @@ pub fn use_messages() -> MessagesState {
|
|||||||
// Reanalyze all error messages
|
// Reanalyze all error messages
|
||||||
let reanalyze_all_errors_impl = Arc::new(move || {
|
let reanalyze_all_errors_impl = Arc::new(move || {
|
||||||
spawn_local(async move {
|
spawn_local(async move {
|
||||||
|
log_info!("Messages reanalyze all errors start");
|
||||||
match reanalyze_batch().await {
|
match reanalyze_batch().await {
|
||||||
Ok(_count) => {
|
Ok(_count) => {
|
||||||
|
log_info!("Messages reanalyze all errors OK: count={}", _count);
|
||||||
error_signal.set(None);
|
error_signal.set(None);
|
||||||
// Optimistically mark all error messages as Processing
|
// Optimistically mark all error messages as Processing
|
||||||
let mut msgs = messages_signal.get();
|
let mut msgs = messages_signal.get();
|
||||||
@@ -192,6 +206,7 @@ pub fn use_messages() -> MessagesState {
|
|||||||
messages_signal.set(msgs);
|
messages_signal.set(msgs);
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
log_info!("Messages reanalyze all errors failed: {}", e);
|
||||||
error_signal.set(Some(format!("Batch reanalyze failed: {}", e)));
|
error_signal.set(Some(format!("Batch reanalyze failed: {}", e)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ use leptos::prelude::*;
|
|||||||
use shared_types::message::{AiStatus, MessageRecord, PageResult};
|
use shared_types::message::{AiStatus, MessageRecord, PageResult};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use wasm_bindgen_futures::spawn_local;
|
use wasm_bindgen_futures::spawn_local;
|
||||||
|
use crate::{log_debug, log_error, log_info, log_trace, log_warn, make_logger};
|
||||||
|
|
||||||
|
make_logger!();
|
||||||
|
|
||||||
/// Threads whose messages should be hidden from both the feed and the
|
/// Threads whose messages should be hidden from both the feed and the
|
||||||
/// Images tab. A bot or selfbot may be spamming in a thread, polluting
|
/// Images tab. A bot or selfbot may be spamming in a thread, polluting
|
||||||
@@ -109,13 +112,16 @@ pub fn MessagesPanel() -> impl IntoView {
|
|||||||
}
|
}
|
||||||
set_is_searching.set(true);
|
set_is_searching.set(true);
|
||||||
let q_clone = query.trim().to_string();
|
let q_clone = query.trim().to_string();
|
||||||
|
log_info!("Messages searching for: {}", q_clone);
|
||||||
spawn_local(async move {
|
spawn_local(async move {
|
||||||
match crate::api::messages::search_messages(&q_clone, Some(50)).await {
|
match crate::api::messages::search_messages(&q_clone, Some(50)).await {
|
||||||
Ok(results) => {
|
Ok(results) => {
|
||||||
|
log_info!("Messages search found {} results", results.len());
|
||||||
set_search_results.set(results);
|
set_search_results.set(results);
|
||||||
set_show_search.set(true);
|
set_show_search.set(true);
|
||||||
}
|
}
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
|
log_warn!("Messages search failed");
|
||||||
set_search_results.set(Vec::new());
|
set_search_results.set(Vec::new());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -143,10 +149,12 @@ pub fn MessagesPanel() -> impl IntoView {
|
|||||||
// WS event handlers (wire once on mount)
|
// WS event handlers (wire once on mount)
|
||||||
let ws = use_context::<crate::ws::context::WsContext>();
|
let ws = use_context::<crate::ws::context::WsContext>();
|
||||||
if let Some(ref ws) = ws {
|
if let Some(ref ws) = ws {
|
||||||
|
log_info!("MessagesPanel wiring WS handlers");
|
||||||
// Subscribe to real-time message events
|
// Subscribe to real-time message events
|
||||||
{
|
{
|
||||||
let msgs = state.messages;
|
let msgs = state.messages;
|
||||||
*ws.on_message_created.borrow_mut() = Some(Box::new(move |msg| {
|
*ws.on_message_created.borrow_mut() = Some(Box::new(move |msg| {
|
||||||
|
log_debug!("WS message_created received: {}", msg.id);
|
||||||
let current = msgs.get();
|
let current = msgs.get();
|
||||||
msgs.set(merge_messages(¤t, &[msg]));
|
msgs.set(merge_messages(¤t, &[msg]));
|
||||||
}));
|
}));
|
||||||
@@ -154,6 +162,7 @@ pub fn MessagesPanel() -> impl IntoView {
|
|||||||
{
|
{
|
||||||
let msgs = state.messages;
|
let msgs = state.messages;
|
||||||
*ws.on_message_updated.borrow_mut() = Some(Box::new(move |msg| {
|
*ws.on_message_updated.borrow_mut() = Some(Box::new(move |msg| {
|
||||||
|
log_debug!("WS message_updated received: {}", msg.id);
|
||||||
let current = msgs.get();
|
let current = msgs.get();
|
||||||
msgs.set(merge_messages(¤t, &[msg]));
|
msgs.set(merge_messages(¤t, &[msg]));
|
||||||
}));
|
}));
|
||||||
@@ -161,6 +170,7 @@ pub fn MessagesPanel() -> impl IntoView {
|
|||||||
{
|
{
|
||||||
let msgs = state.messages;
|
let msgs = state.messages;
|
||||||
*ws.on_message_deleted.borrow_mut() = Some(Box::new(move |id| {
|
*ws.on_message_deleted.borrow_mut() = Some(Box::new(move |id| {
|
||||||
|
log_debug!("WS message_deleted received: {}", id);
|
||||||
let current = msgs.get();
|
let current = msgs.get();
|
||||||
msgs.set(current.into_iter().filter(|m| m.id != id).collect());
|
msgs.set(current.into_iter().filter(|m| m.id != id).collect());
|
||||||
}));
|
}));
|
||||||
@@ -168,6 +178,7 @@ pub fn MessagesPanel() -> impl IntoView {
|
|||||||
{
|
{
|
||||||
let msgs = state.messages;
|
let msgs = state.messages;
|
||||||
*ws.on_message_analyzed.borrow_mut() = Some(Box::new(move |msg| {
|
*ws.on_message_analyzed.borrow_mut() = Some(Box::new(move |msg| {
|
||||||
|
log_debug!("WS message_analyzed received: {}", msg.id);
|
||||||
let current = msgs.get();
|
let current = msgs.get();
|
||||||
msgs.set(merge_messages(¤t, &[msg]));
|
msgs.set(merge_messages(¤t, &[msg]));
|
||||||
}));
|
}));
|
||||||
@@ -191,15 +202,13 @@ pub fn MessagesPanel() -> impl IntoView {
|
|||||||
let guild_id = use_context::<crate::app::AppConfig>()
|
let guild_id = use_context::<crate::app::AppConfig>()
|
||||||
.and_then(|c| c.monitor_guild_id.get());
|
.and_then(|c| c.monitor_guild_id.get());
|
||||||
if let Some(gid) = guild_id {
|
if let Some(gid) = guild_id {
|
||||||
|
log_info!("Messages fetching images for guild {}", gid);
|
||||||
spawn_local({
|
spawn_local({
|
||||||
let image_messages = image_messages.clone();
|
let image_messages = image_messages.clone();
|
||||||
async move {
|
async move {
|
||||||
match crate::api::messages::get_images(&gid, Some(100)).await {
|
match crate::api::messages::get_images(&gid, Some(100)).await {
|
||||||
Ok(PageResult { data, .. }) => {
|
Ok(PageResult { data, .. }) => {
|
||||||
web_sys::console::log_2(
|
log_info!("Messages images loaded: count={}", data.len());
|
||||||
&"[images] fetch OK".into(),
|
|
||||||
&format!("count={}", data.len()).into(),
|
|
||||||
);
|
|
||||||
image_messages.set(
|
image_messages.set(
|
||||||
data.into_iter()
|
data.into_iter()
|
||||||
.filter(|m| !is_excluded_thread(m))
|
.filter(|m| !is_excluded_thread(m))
|
||||||
@@ -207,10 +216,7 @@ pub fn MessagesPanel() -> impl IntoView {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
web_sys::console::log_2(
|
log_error!("Messages images error: {}", e);
|
||||||
&"[images] fetch ERROR".into(),
|
|
||||||
&format!("{}", e).into(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
use crate::features::polish::{persist_theme, ThemeContext};
|
use crate::features::polish::{persist_theme, ThemeContext};
|
||||||
use leptos::prelude::*;
|
use leptos::prelude::*;
|
||||||
|
use crate::{log_info, make_logger};
|
||||||
|
|
||||||
|
make_logger!();
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn ThemeToggle() -> impl IntoView {
|
pub fn ThemeToggle() -> impl IntoView {
|
||||||
@@ -21,6 +24,7 @@ pub fn ThemeToggle() -> impl IntoView {
|
|||||||
} else {
|
} else {
|
||||||
"dark"
|
"dark"
|
||||||
};
|
};
|
||||||
|
log_info!("Theme toggled to {}", next);
|
||||||
ctx.theme.set(next.to_string());
|
ctx.theme.set(next.to_string());
|
||||||
persist_theme(next);
|
persist_theme(next);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
pub mod components;
|
pub mod components;
|
||||||
|
|
||||||
use leptos::prelude::*;
|
use leptos::prelude::*;
|
||||||
|
use crate::{log_info, make_logger};
|
||||||
|
|
||||||
|
make_logger!();
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct ThemeContext {
|
pub struct ThemeContext {
|
||||||
@@ -8,14 +11,17 @@ pub struct ThemeContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn initial_theme() -> String {
|
pub fn initial_theme() -> String {
|
||||||
web_sys::window()
|
let theme = web_sys::window()
|
||||||
.and_then(|window| window.local_storage().ok().flatten())
|
.and_then(|window| window.local_storage().ok().flatten())
|
||||||
.and_then(|storage| storage.get_item("imphnen-theme").ok().flatten())
|
.and_then(|storage| storage.get_item("imphnen-theme").ok().flatten())
|
||||||
.filter(|value| value == "dark" || value == "light")
|
.filter(|value| value == "dark" || value == "light")
|
||||||
.unwrap_or_else(|| "light".to_string())
|
.unwrap_or_else(|| "light".to_string());
|
||||||
|
log_info!("Initial theme resolved: {}", theme);
|
||||||
|
theme
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn persist_theme(theme: &str) {
|
pub fn persist_theme(theme: &str) {
|
||||||
|
log_info!("Persisting theme: {}", theme);
|
||||||
if let Some(storage) =
|
if let Some(storage) =
|
||||||
web_sys::window().and_then(|window| window.local_storage().ok().flatten())
|
web_sys::window().and_then(|window| window.local_storage().ok().flatten())
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ use super::tab_strip::TabStrip;
|
|||||||
use leptos::children::Children;
|
use leptos::children::Children;
|
||||||
use leptos::prelude::*;
|
use leptos::prelude::*;
|
||||||
|
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn DashboardLayout(children: Children) -> impl IntoView {
|
pub fn DashboardLayout(children: Children) -> impl IntoView {
|
||||||
view! {
|
view! {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ use crate::ws::context::WsContext;
|
|||||||
use crate::ws::socket::WsStatus;
|
use crate::ws::socket::WsStatus;
|
||||||
use leptos::prelude::*;
|
use leptos::prelude::*;
|
||||||
|
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn Header() -> impl IntoView {
|
pub fn Header() -> impl IntoView {
|
||||||
let ws = use_context::<WsContext>().expect("WsContext not provided");
|
let ws = use_context::<WsContext>().expect("WsContext not provided");
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ use crate::app::UiContext;
|
|||||||
use leptos::prelude::*;
|
use leptos::prelude::*;
|
||||||
use shared_types::ui_state::Tab;
|
use shared_types::ui_state::Tab;
|
||||||
|
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn MobileTabBar() -> impl IntoView {
|
pub fn MobileTabBar() -> impl IntoView {
|
||||||
let ui = use_context::<UiContext>().expect("UiContext not provided");
|
let ui = use_context::<UiContext>().expect("UiContext not provided");
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ use crate::app::UiContext;
|
|||||||
use leptos::prelude::*;
|
use leptos::prelude::*;
|
||||||
use shared_types::ui_state::Tab;
|
use shared_types::ui_state::Tab;
|
||||||
|
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn Sidebar() -> impl IntoView {
|
pub fn Sidebar() -> impl IntoView {
|
||||||
let ui = use_context::<UiContext>().expect("UiContext not provided");
|
let ui = use_context::<UiContext>().expect("UiContext not provided");
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ use crate::app::UiContext;
|
|||||||
use leptos::prelude::*;
|
use leptos::prelude::*;
|
||||||
use shared_types::ui_state::Tab;
|
use shared_types::ui_state::Tab;
|
||||||
|
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn TabStrip() -> impl IntoView {
|
pub fn TabStrip() -> impl IntoView {
|
||||||
let ui = use_context::<UiContext>().expect("UiContext not provided");
|
let ui = use_context::<UiContext>().expect("UiContext not provided");
|
||||||
|
|||||||
@@ -3,18 +3,19 @@ pub mod app;
|
|||||||
pub mod auth;
|
pub mod auth;
|
||||||
pub mod features;
|
pub mod features;
|
||||||
pub mod layout;
|
pub mod layout;
|
||||||
|
pub mod logger;
|
||||||
pub mod ui;
|
pub mod ui;
|
||||||
pub mod ws;
|
pub mod ws;
|
||||||
|
|
||||||
|
|
||||||
use wasm_bindgen::prelude::*;
|
use wasm_bindgen::prelude::*;
|
||||||
|
|
||||||
|
make_logger!();
|
||||||
|
|
||||||
#[wasm_bindgen(start)]
|
#[wasm_bindgen(start)]
|
||||||
pub fn start() {
|
pub fn start() {
|
||||||
// Set up panic hook for better error messages in the browser console
|
|
||||||
console_error_panic_hook::set_once();
|
console_error_panic_hook::set_once();
|
||||||
// Initialize logger
|
|
||||||
wasm_logger::init(wasm_logger::Config::default());
|
wasm_logger::init(wasm_logger::Config::default());
|
||||||
|
log_info!("IMPHNEN frontend starting...");
|
||||||
// Mount the Leptos app to the body
|
|
||||||
leptos::mount::mount_to_body(app::App);
|
leptos::mount::mount_to_body(app::App);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,177 @@
|
|||||||
|
// services/frontend/frontend/src/logger.rs
|
||||||
|
// Structured logging for WASM browser console with levels, timestamps, and styled output.
|
||||||
|
|
||||||
|
/// Log level with numeric priority (lower = more verbose).
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
pub enum LogLevel {
|
||||||
|
Trace = 0,
|
||||||
|
Debug = 1,
|
||||||
|
Info = 2,
|
||||||
|
Warn = 3,
|
||||||
|
Error = 4,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LogLevel {
|
||||||
|
fn as_str(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
LogLevel::Trace => "TRACE",
|
||||||
|
LogLevel::Debug => "DEBUG",
|
||||||
|
LogLevel::Info => "INFO",
|
||||||
|
LogLevel::Warn => "WARN",
|
||||||
|
LogLevel::Error => "ERROR",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// CSS color for the browser console label.
|
||||||
|
fn console_style(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
LogLevel::Trace => "color:#888",
|
||||||
|
LogLevel::Debug => "color:#54a2ff",
|
||||||
|
LogLevel::Info => "color:#23a1eb;font-weight:bold",
|
||||||
|
LogLevel::Warn => "color:#f59e0b;font-weight:bold",
|
||||||
|
LogLevel::Error => "color:#e4405f;font-weight:bold",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A per-module logger that produces styled, timestamped console output.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Logger {
|
||||||
|
module: &'static str,
|
||||||
|
min_level: LogLevel,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Logger {
|
||||||
|
/// Create a logger for a given module path (call with `module_path!()`).
|
||||||
|
pub const fn new(module: &'static str, min_level: LogLevel) -> Self {
|
||||||
|
Self { module, min_level }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a logger that shows everything (min_level = Trace).
|
||||||
|
pub const fn verbose(module: &'static str) -> Self {
|
||||||
|
Self::new(module, LogLevel::Trace)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Format an ISO-like timestamp from `Date.now()`.
|
||||||
|
fn timestamp() -> String {
|
||||||
|
let d = js_sys::Date::new_0();
|
||||||
|
// HH:MM:SS.mmm
|
||||||
|
format!(
|
||||||
|
"{:02}:{:02}:{:02}.{:03}",
|
||||||
|
d.get_hours(),
|
||||||
|
d.get_minutes(),
|
||||||
|
d.get_seconds(),
|
||||||
|
d.get_milliseconds()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn should_log(&self, level: LogLevel) -> bool {
|
||||||
|
level >= self.min_level
|
||||||
|
}
|
||||||
|
|
||||||
|
fn log_inner(&self, level: LogLevel, message: &str) {
|
||||||
|
if !self.should_log(level) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let ts = Self::timestamp();
|
||||||
|
let lvl_str = level.as_str();
|
||||||
|
let style = level.console_style();
|
||||||
|
let styled = format!("%c{:.7} [{}] {}", ts, self.module, message);
|
||||||
|
match level {
|
||||||
|
LogLevel::Error => {
|
||||||
|
web_sys::console::error_3(
|
||||||
|
&styled.into(),
|
||||||
|
&style.into(),
|
||||||
|
&"".into(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
LogLevel::Warn => {
|
||||||
|
web_sys::console::warn_3(
|
||||||
|
&styled.into(),
|
||||||
|
&style.into(),
|
||||||
|
&"".into(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
web_sys::console::log_3(
|
||||||
|
&styled.into(),
|
||||||
|
&style.into(),
|
||||||
|
&"".into(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn trace(&self, msg: &str) {
|
||||||
|
self.log_inner(LogLevel::Trace, msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn debug(&self, msg: &str) {
|
||||||
|
self.log_inner(LogLevel::Debug, msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn info(&self, msg: &str) {
|
||||||
|
self.log_inner(LogLevel::Info, msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn warn(&self, msg: &str) {
|
||||||
|
self.log_inner(LogLevel::Warn, msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn error(&self, msg: &str) {
|
||||||
|
self.log_inner(LogLevel::Error, msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Log with a dynamic format string.
|
||||||
|
pub fn info_fmt(&self, fmt: &str, args: &[&dyn std::fmt::Display]) {
|
||||||
|
let msg = if args.is_empty() {
|
||||||
|
fmt.to_string()
|
||||||
|
} else {
|
||||||
|
let mut s = String::new();
|
||||||
|
let mut iter = args.iter();
|
||||||
|
for part in fmt.split("{}") {
|
||||||
|
s.push_str(part);
|
||||||
|
if let Some(arg) = iter.next() {
|
||||||
|
s.push_str(&arg.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s
|
||||||
|
};
|
||||||
|
self.log_inner(LogLevel::Info, &msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Macro to create a module-level logger at `Info` level.
|
||||||
|
/// Usage: `log::module!()` at the top of a source file (after imports).
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! make_logger {
|
||||||
|
() => {
|
||||||
|
static LOGGER: std::sync::LazyLock<$crate::logger::Logger> =
|
||||||
|
std::sync::LazyLock::new(|| {
|
||||||
|
$crate::logger::Logger::new(module_path!(), $crate::logger::LogLevel::Trace)
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convenience macros that log through the module's static LOGGER.
|
||||||
|
/// Usage: `log_info!("something happened")`.
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! log_trace {
|
||||||
|
($($arg:tt)*) => { LOGGER.trace(&format!($($arg)*)); };
|
||||||
|
}
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! log_debug {
|
||||||
|
($($arg:tt)*) => { LOGGER.debug(&format!($($arg)*)); };
|
||||||
|
}
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! log_info {
|
||||||
|
($($arg:tt)*) => { LOGGER.info(&format!($($arg)*)); };
|
||||||
|
}
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! log_warn {
|
||||||
|
($($arg:tt)*) => { LOGGER.warn(&format!($($arg)*)); };
|
||||||
|
}
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! log_error {
|
||||||
|
($($arg:tt)*) => { LOGGER.error(&format!($($arg)*)); };
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
use leptos::prelude::*;
|
use leptos::prelude::*;
|
||||||
|
|
||||||
|
|
||||||
#[derive(Clone, Default)]
|
#[derive(Clone, Default)]
|
||||||
pub enum ButtonVariant {
|
pub enum ButtonVariant {
|
||||||
#[default]
|
#[default]
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use leptos::prelude::*;
|
use leptos::prelude::*;
|
||||||
|
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn Card(
|
pub fn Card(
|
||||||
#[prop(optional)] elevated: bool,
|
#[prop(optional)] elevated: bool,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// services/frontend-leptos/frontend/src/ui/empty_state.rs
|
// services/frontend-leptos/frontend/src/ui/empty_state.rs
|
||||||
use leptos::prelude::*;
|
use leptos::prelude::*;
|
||||||
|
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn EmptyState(
|
pub fn EmptyState(
|
||||||
#[prop(optional)] icon: Option<AnyView>,
|
#[prop(optional)] icon: Option<AnyView>,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
use leptos::prelude::*;
|
use leptos::prelude::*;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn Modal(
|
pub fn Modal(
|
||||||
is_open: RwSignal<bool>,
|
is_open: RwSignal<bool>,
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
// services/frontend-leptos/frontend/src/ui/toast.rs
|
// services/frontend-leptos/frontend/src/ui/toast.rs
|
||||||
use leptos::prelude::*;
|
use leptos::prelude::*;
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
|
use std::fmt;
|
||||||
|
use crate::{log_info, make_logger};
|
||||||
|
|
||||||
|
make_logger!();
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub enum ToastType {
|
pub enum ToastType {
|
||||||
@@ -10,6 +14,17 @@ pub enum ToastType {
|
|||||||
Warning,
|
Warning,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for ToastType {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
ToastType::Info => write!(f, "info"),
|
||||||
|
ToastType::Success => write!(f, "success"),
|
||||||
|
ToastType::Error => write!(f, "error"),
|
||||||
|
ToastType::Warning => write!(f, "warning"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct ToastMessage {
|
pub struct ToastMessage {
|
||||||
pub id: u64,
|
pub id: u64,
|
||||||
@@ -38,6 +53,7 @@ impl ToastContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn show(&self, message: &str, toast_type: ToastType) {
|
pub fn show(&self, message: &str, toast_type: ToastType) {
|
||||||
|
log_info!("Toast: {} ({})", message, toast_type);
|
||||||
let id = {
|
let id = {
|
||||||
let mut n = self.next_id.lock().unwrap();
|
let mut n = self.next_id.lock().unwrap();
|
||||||
*n += 1;
|
*n += 1;
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ use shared_types::media::MediaState;
|
|||||||
use shared_types::message::MessageRecord;
|
use shared_types::message::MessageRecord;
|
||||||
use shared_types::recording::VoiceRecording;
|
use shared_types::recording::VoiceRecording;
|
||||||
use shared_types::voice::ActiveSpeaker;
|
use shared_types::voice::ActiveSpeaker;
|
||||||
|
use crate::{log_debug, log_error, log_info, log_trace, log_warn, make_logger};
|
||||||
|
|
||||||
|
make_logger!();
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
#[allow(clippy::type_complexity)]
|
#[allow(clippy::type_complexity)]
|
||||||
@@ -61,6 +64,7 @@ impl WsContext {
|
|||||||
|
|
||||||
match event_type.as_str() {
|
match event_type.as_str() {
|
||||||
"message_created" => {
|
"message_created" => {
|
||||||
|
log_debug!("WS event: message_created");
|
||||||
if let Some(d) = data.and_then(|v| {
|
if let Some(d) = data.and_then(|v| {
|
||||||
serde_json::from_value::<MessageRecord>(v.clone()).ok()
|
serde_json::from_value::<MessageRecord>(v.clone()).ok()
|
||||||
}) {
|
}) {
|
||||||
@@ -70,6 +74,7 @@ impl WsContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
"message_updated" => {
|
"message_updated" => {
|
||||||
|
log_debug!("WS event: message_updated");
|
||||||
if let Some(d) = data.and_then(|v| {
|
if let Some(d) = data.and_then(|v| {
|
||||||
serde_json::from_value::<MessageRecord>(v.clone()).ok()
|
serde_json::from_value::<MessageRecord>(v.clone()).ok()
|
||||||
}) {
|
}) {
|
||||||
@@ -79,6 +84,7 @@ impl WsContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
"message_deleted" => {
|
"message_deleted" => {
|
||||||
|
log_debug!("WS event: message_deleted");
|
||||||
if let Some(d) = data.and_then(|v| v.as_str().map(String::from)) {
|
if let Some(d) = data.and_then(|v| v.as_str().map(String::from)) {
|
||||||
if let Some(cb) = self.on_message_deleted.borrow().as_ref() {
|
if let Some(cb) = self.on_message_deleted.borrow().as_ref() {
|
||||||
cb(d);
|
cb(d);
|
||||||
@@ -86,6 +92,7 @@ impl WsContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
"message_analyzed" => {
|
"message_analyzed" => {
|
||||||
|
log_debug!("WS event: message_analyzed");
|
||||||
if let Some(d) = data.and_then(|v| {
|
if let Some(d) = data.and_then(|v| {
|
||||||
serde_json::from_value::<MessageRecord>(v.clone()).ok()
|
serde_json::from_value::<MessageRecord>(v.clone()).ok()
|
||||||
}) {
|
}) {
|
||||||
@@ -95,6 +102,7 @@ impl WsContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
"voice_active_user" => {
|
"voice_active_user" => {
|
||||||
|
log_debug!("WS event: voice_active_user");
|
||||||
if let Some(d) = data.and_then(|v| {
|
if let Some(d) = data.and_then(|v| {
|
||||||
serde_json::from_value::<ActiveSpeaker>(v.clone()).ok()
|
serde_json::from_value::<ActiveSpeaker>(v.clone()).ok()
|
||||||
}) {
|
}) {
|
||||||
@@ -104,6 +112,7 @@ impl WsContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
"voice_recording_uploaded" => {
|
"voice_recording_uploaded" => {
|
||||||
|
log_debug!("WS event: voice_recording_uploaded");
|
||||||
if let Some(d) = data.and_then(|v| {
|
if let Some(d) = data.and_then(|v| {
|
||||||
serde_json::from_value::<VoiceRecording>(v.clone()).ok()
|
serde_json::from_value::<VoiceRecording>(v.clone()).ok()
|
||||||
}) {
|
}) {
|
||||||
@@ -114,6 +123,7 @@ impl WsContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
"media_state" => {
|
"media_state" => {
|
||||||
|
log_debug!("WS event: media_state");
|
||||||
// Backend sends initial state with "state" key, live updates with "data"
|
// Backend sends initial state with "state" key, live updates with "data"
|
||||||
let raw = data.or_else(|| parsed.get("state")).cloned();
|
let raw = data.or_else(|| parsed.get("state")).cloned();
|
||||||
if let Some(d) =
|
if let Some(d) =
|
||||||
@@ -126,14 +136,13 @@ impl WsContext {
|
|||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
// Unknown event type — log and ignore
|
// Unknown event type — log and ignore
|
||||||
web_sys::console::log_1(
|
log_warn!("WS unhandled event type: {}", event_type);
|
||||||
&format!("[WS] unhandled event: {}", event_type).into(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
WsEvent::Binary(data) => {
|
WsEvent::Binary(data) => {
|
||||||
|
log_debug!("WS event: binary ({} bytes)", data.len());
|
||||||
if let Some(cb) = self.on_binary.borrow().as_ref() {
|
if let Some(cb) = self.on_binary.borrow().as_ref() {
|
||||||
cb(data);
|
cb(data);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ use leptos::prelude::*;
|
|||||||
use wasm_bindgen::prelude::*;
|
use wasm_bindgen::prelude::*;
|
||||||
use wasm_bindgen::JsCast;
|
use wasm_bindgen::JsCast;
|
||||||
use web_sys::{CloseEvent, ErrorEvent, MessageEvent, WebSocket};
|
use web_sys::{CloseEvent, ErrorEvent, MessageEvent, WebSocket};
|
||||||
|
use crate::{log_debug, log_error, log_info, log_trace, log_warn, make_logger};
|
||||||
|
|
||||||
|
make_logger!();
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub enum WsStatus {
|
pub enum WsStatus {
|
||||||
@@ -85,6 +88,7 @@ impl WsHandle {
|
|||||||
reconnect_attempt: *const std::cell::Cell<u32>,
|
reconnect_attempt: *const std::cell::Cell<u32>,
|
||||||
) {
|
) {
|
||||||
let url_owned = url.to_string();
|
let url_owned = url.to_string();
|
||||||
|
let url_close = url_owned.clone();
|
||||||
let status1 = set_status;
|
let status1 = set_status;
|
||||||
let status2 = set_status;
|
let status2 = set_status;
|
||||||
let status3 = set_status;
|
let status3 = set_status;
|
||||||
@@ -98,6 +102,7 @@ impl WsHandle {
|
|||||||
// onopen
|
// onopen
|
||||||
let onopen_cb = Closure::<dyn Fn(web_sys::ProgressEvent)>::new(move |_| {
|
let onopen_cb = Closure::<dyn Fn(web_sys::ProgressEvent)>::new(move |_| {
|
||||||
status1.set(WsStatus::Connected);
|
status1.set(WsStatus::Connected);
|
||||||
|
log_info!("WS connected to {}", url_owned);
|
||||||
unsafe { (*reconnect_attempt).set(0) };
|
unsafe { (*reconnect_attempt).set(0) };
|
||||||
});
|
});
|
||||||
ws.set_onopen(Some(onopen_cb.as_ref().unchecked_ref()));
|
ws.set_onopen(Some(onopen_cb.as_ref().unchecked_ref()));
|
||||||
@@ -107,6 +112,7 @@ impl WsHandle {
|
|||||||
let event_for_close = event_clone.clone();
|
let event_for_close = event_clone.clone();
|
||||||
let onclose_cb = Closure::<dyn Fn(CloseEvent)>::new(move |_| {
|
let onclose_cb = Closure::<dyn Fn(CloseEvent)>::new(move |_| {
|
||||||
status2.set(WsStatus::Disconnected);
|
status2.set(WsStatus::Disconnected);
|
||||||
|
log_info!("WS disconnected from {}", url_close);
|
||||||
unsafe { *(*ws_holder).borrow_mut() = None };
|
unsafe { *(*ws_holder).borrow_mut() = None };
|
||||||
|
|
||||||
let attempt = unsafe { (*reconnect_attempt).get() };
|
let attempt = unsafe { (*reconnect_attempt).get() };
|
||||||
@@ -114,6 +120,7 @@ impl WsHandle {
|
|||||||
status2.set(WsStatus::Error(
|
status2.set(WsStatus::Error(
|
||||||
"Max reconnect attempts reached".to_string(),
|
"Max reconnect attempts reached".to_string(),
|
||||||
));
|
));
|
||||||
|
log_error!("WS reconnect max attempts reached for {}", url_close);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Full-jitter exponential backoff: min(1000 * 2^attempt, 30000) * (0.5 + random * 0.5)
|
// Full-jitter exponential backoff: min(1000 * 2^attempt, 30000) * (0.5 + random * 0.5)
|
||||||
@@ -122,7 +129,9 @@ impl WsHandle {
|
|||||||
let delay_ms = (base as f64 * jitter) as u32;
|
let delay_ms = (base as f64 * jitter) as u32;
|
||||||
unsafe { (*reconnect_attempt).set(attempt + 1) };
|
unsafe { (*reconnect_attempt).set(attempt + 1) };
|
||||||
|
|
||||||
let url_reconnect = url_owned.clone();
|
log_info!("WS reconnecting to {} in {}ms (attempt {})", url_close, delay_ms, attempt + 1);
|
||||||
|
|
||||||
|
let url_reconnect = url_close.clone();
|
||||||
let status_rc = status2;
|
let status_rc = status2;
|
||||||
let event_rc = event_for_close.clone();
|
let event_rc = event_for_close.clone();
|
||||||
let reconnect_fn = Closure::<dyn Fn()>::new(move || {
|
let reconnect_fn = Closure::<dyn Fn()>::new(move || {
|
||||||
@@ -148,6 +157,7 @@ impl WsHandle {
|
|||||||
|
|
||||||
// onerror
|
// onerror
|
||||||
let onerror_cb = Closure::<dyn Fn(ErrorEvent)>::new(move |e: ErrorEvent| {
|
let onerror_cb = Closure::<dyn Fn(ErrorEvent)>::new(move |e: ErrorEvent| {
|
||||||
|
log_error!("WS error: {}", e.message());
|
||||||
status3.set(WsStatus::Error(e.message()));
|
status3.set(WsStatus::Error(e.message()));
|
||||||
});
|
});
|
||||||
ws.set_onerror(Some(onerror_cb.as_ref().unchecked_ref()));
|
ws.set_onerror(Some(onerror_cb.as_ref().unchecked_ref()));
|
||||||
@@ -173,12 +183,12 @@ impl WsHandle {
|
|||||||
onmsg_cb.forget();
|
onmsg_cb.forget();
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
set_status.set(WsStatus::Error(
|
let msg = js_sys::Error::from(e)
|
||||||
js_sys::Error::from(e)
|
.to_string()
|
||||||
.to_string()
|
.as_string()
|
||||||
.as_string()
|
.unwrap_or_default();
|
||||||
.unwrap_or_default(),
|
log_error!("WS connect failed: {}", msg);
|
||||||
));
|
set_status.set(WsStatus::Error(msg));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user