diff --git a/public/lang/en.json b/public/lang/en.json index c7861443..c8b6f5b6 100644 --- a/public/lang/en.json +++ b/public/lang/en.json @@ -1867,6 +1867,8 @@ }, "debugging": { "title": "Debugging", + "fake_orders_tooltip": "Toggle Fake Orders for Live Trading (For Testing Purposes Only) will be stored in dev.kenya.quantframe\\fake_orders", + "fake_orders_label": "Use Fake Orders Live orders", "datatable": { "columns": { "wfm_url": { diff --git a/src-tauri/src/app/types/debugging_settings.rs b/src-tauri/src/app/types/debugging_settings.rs index 37dfcdb3..a4a89435 100644 --- a/src-tauri/src/app/types/debugging_settings.rs +++ b/src-tauri/src/app/types/debugging_settings.rs @@ -18,12 +18,14 @@ impl Default for DebuggingSettings { #[derive(Clone, Debug, Serialize, Deserialize)] pub struct DebuggingLiveScraperSettings { pub entries: Vec, + pub fake_orders: bool, } impl Default for DebuggingLiveScraperSettings { fn default() -> Self { DebuggingLiveScraperSettings { entries: Vec::new(), + fake_orders: false, } } } diff --git a/src-tauri/src/handlers/base.rs b/src-tauri/src/handlers/base.rs index 982d5959..c9b78a47 100644 --- a/src-tauri/src/handlers/base.rs +++ b/src-tauri/src/handlers/base.rs @@ -18,6 +18,7 @@ pub async fn handle_wfm_item( ) -> Result { let wfm_id = wfm_id.into(); let app = states::app_state()?; + let settings = &app.settings.live_scraper; let component = "HandleWFMItem"; let file = "handle_wfm_item.log"; let mut operation_status = "NoOrder".to_string(); diff --git a/src-tauri/src/live_scraper/modules/helpers.rs b/src-tauri/src/live_scraper/modules/helpers.rs index e0370dee..9b0ba861 100644 --- a/src-tauri/src/live_scraper/modules/helpers.rs +++ b/src-tauri/src/live_scraper/modules/helpers.rs @@ -1,6 +1,7 @@ -use std::{collections::HashMap, sync::OnceLock}; +use std::{collections::HashMap, path::Path, sync::OnceLock}; use entity::{stock_item::*, wish_list::*}; +use qf_api::errors::ApiError; use serde_json::json; use service::*; use utils::*; @@ -442,3 +443,34 @@ pub async fn progress_order( } Ok(()) } + +pub async fn fetch_and_cache_orders( + component: &str, + wfm_client: &wf_market::Client, + item_url: &str, + cache_path: Option<&Path>, +) -> Result, Error> { + let orders = wfm_client + .order() + .get_orders_by_item(item_url) + .await + .map_err(|e| { + let log_level = match e { + wf_market::errors::ApiError::RequestError(_) => LogLevel::Error, + _ => LogLevel::Critical, + }; + Error::from_wfm( + format!("{}:FetchAndCacheOrders", component), + &format!("Failed to get live orders for item {}", item_url), + e, + get_location!(), + ) + .set_log_level(log_level) + })?; + + if let Some(path) = cache_path { + utils::write_json_file(path, &orders)?; + } + + Ok(orders) +} diff --git a/src-tauri/src/live_scraper/modules/item.rs b/src-tauri/src/live_scraper/modules/item.rs index 938eddf5..6346a42c 100644 --- a/src-tauri/src/live_scraper/modules/item.rs +++ b/src-tauri/src/live_scraper/modules/item.rs @@ -1,5 +1,6 @@ use std::{ collections::HashSet, + path::PathBuf, sync::{atomic::Ordering, Arc, Weak}, }; @@ -152,6 +153,7 @@ impl ItemModule { ) -> Result<(), Error> { let cache = states::cache_client()?; let client = self.client.upgrade().expect("Client should not be dropped"); + let use_fake = app.settings.debugging.live_scraper.fake_orders; let mut current_index = 1; // Sort by priority (highest first) @@ -197,27 +199,50 @@ impl ItemModule { })), ); - // Fetch live orders from API - let mut orders = match app - .wfm_client - .order() - .get_orders_by_item(&item_entry.wfm_url) - .await - { - Ok(o) => o, - Err(e) => { - let log_level = match e { - ApiError::RequestError(_) => LogLevel::Error, - _ => LogLevel::Critical, - }; - return Err(Error::from_wfm( - format!("{}ProcessItem", COMPONENT), - &format!("Failed to get live orders for item {}", item_entry.wfm_url), - e, - get_location!(), - ) - .set_log_level(log_level)); + let order_path = PathBuf::from(utils::get_base_path()) + .join("fake_orders") + .join(format!("order_{}.json", item_info.wfm_url_name)); + + let mut orders = if use_fake && order_path.exists() { + match utils::read_json_file::>(&order_path) { + Ok(cached) => { + info( + format!("{}ProcessItem", COMPONENT), + &format!( + "Using cached fake orders for item {} from {}", + item_entry.wfm_url, + order_path.display() + ), + &&LoggerOptions::default(), + ); + cached + } + Err(e) => { + warning( + format!("{}ProcessItem", COMPONENT), + &format!( + "Failed to read fake orders for item {} ({}), falling back to API", + item_entry.wfm_url, e + ), + &&LoggerOptions::default(), + ); + fetch_and_cache_orders( + &format!("{}ProcessItem", COMPONENT), + &app.wfm_client, + &item_entry.wfm_url, + use_fake.then_some(&order_path), + ) + .await? + } } + } else { + fetch_and_cache_orders( + &format!("{}ProcessItem", COMPONENT), + &app.wfm_client, + &item_entry.wfm_url, + use_fake.then_some(&order_path), + ) + .await? }; // Apply filters to orders @@ -425,10 +450,44 @@ impl ItemModule { post_price, )); + let mut knapsack_skip_reasons = Vec::new(); + if closed_avg_metric < 0 { + knapsack_skip_reasons.push("ClosedAvgMetric<0"); + } + + if price_range < profit_threshold { + knapsack_skip_reasons.push("PriceRangeBelowProfitThreshold"); + } + + if !order_info.has_operation("Create") { + knapsack_skip_reasons.push("NoCreateOperation"); + } + + let has_buy_orders = !wfm_client.order().cache_orders().buy_orders.is_empty(); + if !has_buy_orders { + knapsack_skip_reasons.push("NoExistingBuyOrders"); + } + + if is_disabled(max_total_price_cap) { + knapsack_skip_reasons.push("MaxTotalPriceCapDisabled"); + } + + if !knapsack_skip_reasons.is_empty() { + info( + format!("{}KnapsackSkip", component), + &format!( + "Knapsack skipped for item {}: {}", + item_info.name, + knapsack_skip_reasons.join(", ") + ), + &log_options, + ); + } + if closed_avg_metric >= 0 && price_range >= profit_threshold && order_info.has_operation("Create") - && !wfm_client.order().cache_orders().buy_orders.is_empty() + && has_buy_orders && !is_disabled(max_total_price_cap) { let buy_orders_list = { @@ -491,10 +550,10 @@ impl ItemModule { order_info.add_operation("Skip"); order_info.add_operation("Delete"); } - } else if closed_avg_metric < 0 && !is_disabled(max_total_price_cap) { + } else if closed_avg_metric < 0 { order_info.add_operation("Delete"); order_info.add_operation("Overpriced"); - } else if price_range < profit_threshold && !is_disabled(max_total_price_cap) { + } else if price_range < profit_threshold { order_info.add_operation("Delete"); order_info.add_operation("Underpriced"); } diff --git a/src-tauri/utils/src/helper.rs b/src-tauri/utils/src/helper.rs index 8790af1f..8d86c861 100644 --- a/src-tauri/utils/src/helper.rs +++ b/src-tauri/utils/src/helper.rs @@ -116,6 +116,27 @@ pub fn read_json_file(path: &PathBuf) -> Result< )), } } + +/** + * Writes a serializable object to a JSON file at the specified path. + * Creates parent directories if they do not exist. + * # Arguments + * * `path` - The file path to write the JSON data to + * * `data` - The serializable object to write + */ +pub fn write_json_file( + path: impl AsRef, + data: &T, +) -> std::io::Result<()> { + let path_ref = path.as_ref(); + // Check if the folder exists + if let Some(parent) = path_ref.parent() { + std::fs::create_dir_all(parent)?; + } + let file = std::fs::File::create(path_ref)?; + serde_json::to_writer(file, data)?; + Ok(()) +} /// Find an object in a Vec by multiple criteria using a predicate function /// /// # Arguments diff --git a/src/components/DataDisplay/ChatMessage/ChatMessage.tsx b/src/components/DataDisplay/ChatMessage/ChatMessage.tsx index 410a6d2e..fca70c4e 100644 --- a/src/components/DataDisplay/ChatMessage/ChatMessage.tsx +++ b/src/components/DataDisplay/ChatMessage/ChatMessage.tsx @@ -6,6 +6,7 @@ import { useEffect, useState } from "react"; import dayjs from "dayjs"; import calendar from "dayjs/plugin/calendar"; dayjs.extend(calendar); +import { decodeHtmlEntities } from "@utils/helper"; export type ChatMessageProps = { user: WFMarketTypes.User | undefined; @@ -42,7 +43,7 @@ export const ChatMessage = ({ user, msg, sender }: ChatMessageProps) => { setOpen((o) => !o); }} > - {msg.raw_message} + {decodeHtmlEntities(msg.raw_message)} diff --git a/src/pages/debug/tabs/debugging/index.tsx b/src/pages/debug/tabs/debugging/index.tsx index 783c237f..65bc86fe 100644 --- a/src/pages/debug/tabs/debugging/index.tsx +++ b/src/pages/debug/tabs/debugging/index.tsx @@ -1,4 +1,4 @@ -import { Box, Group } from "@mantine/core"; +import { Box, Checkbox, Group, Tooltip } from "@mantine/core"; import { DataTable } from "mantine-datatable"; import { useTranslatePages } from "@hooks/useTranslate.hook"; import { useHasAlert } from "@hooks/useHasAlert.hook"; @@ -23,17 +23,34 @@ export const DebuggingPanel = ({}: DebuggingPanelProps) => { return ( - { - if (!settings) return; - let items = [...(settings?.debugging.live_scraper.entries || []), values]; - await api.app.updateSettings({ - ...settings, - debugging: { ...settings.debugging, live_scraper: { ...settings.debugging.live_scraper, entries: items } }, - }); - SendTauriEvent(TauriTypes.Events.RefreshSettings); - }} - /> + + { + if (!settings) return; + let items = [...(settings?.debugging.live_scraper.entries || []), values]; + await api.app.updateSettings({ + ...settings, + debugging: { ...settings.debugging, live_scraper: { ...settings.debugging.live_scraper, entries: items } }, + }); + SendTauriEvent(TauriTypes.Events.RefreshSettings); + }} + /> + + { + if (!settings) return; + await api.app.updateSettings({ + ...settings, + debugging: { ...settings.debugging, live_scraper: { ...settings.debugging.live_scraper, fake_orders: e.currentTarget.checked } }, + }); + SendTauriEvent(TauriTypes.Events.RefreshSettings); + }} + /> + + ; } - export interface ChartDto extends ChartWithValuesDto, ChartWithLabelsDto { } - export interface ChartMultipleDto extends ChartWithMultipleValuesDto, ChartWithLabelsDto { } + export interface ChartDto extends ChartWithValuesDto, ChartWithLabelsDto {} + export interface ChartMultipleDto extends ChartWithMultipleValuesDto, ChartWithLabelsDto {} export interface TradingSummaryDto { best_selling_items: TransactionItemSummaryDto[]; category_summary: TransactionCategorySummaryDto[]; @@ -723,7 +724,7 @@ export namespace TauriTypes { name: string; } - export interface StockRivenDetails extends RivenSummary { } + export interface StockRivenDetails extends RivenSummary {} export interface SubType { rank?: number; variant?: string; @@ -828,7 +829,7 @@ export namespace TauriTypes { created_at: string; properties: Record; } - export interface TradeEntryDetails extends TradeEntry { } + export interface TradeEntryDetails extends TradeEntry {} export interface CreateTradeEntry { raw: string; diff --git a/src/utils/helper.ts b/src/utils/helper.ts index d913628a..47e74302 100644 --- a/src/utils/helper.ts +++ b/src/utils/helper.ts @@ -285,6 +285,16 @@ export const GetItemDisplay = ( if ("mod_name" in value) fullName += ` ${value.mod_name}`; return fullName || "Unknown Item"; }; + +export const decodeHtmlEntities = (input: string): string => { + try { + const doc = new DOMParser().parseFromString(input, "text/html"); + return doc.documentElement.textContent || ""; + } catch { + return input; + } +}; + // At the top of your component or in a separate utils file export const getSafePage = (requestedPage: number | undefined, totalPages: number | undefined): number => { const page = requestedPage ?? 1;