Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions public/lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
2 changes: 2 additions & 0 deletions src-tauri/src/app/types/debugging_settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,14 @@ impl Default for DebuggingSettings {
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct DebuggingLiveScraperSettings {
pub entries: Vec<ItemEntry>,
pub fake_orders: bool,
}

impl Default for DebuggingLiveScraperSettings {
fn default() -> Self {
DebuggingLiveScraperSettings {
entries: Vec::new(),
fake_orders: false,
}
}
}
1 change: 1 addition & 0 deletions src-tauri/src/handlers/base.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ pub async fn handle_wfm_item(
) -> Result<String, Error> {
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();
Expand Down
34 changes: 33 additions & 1 deletion src-tauri/src/live_scraper/modules/helpers.rs
Original file line number Diff line number Diff line change
@@ -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::*;
Expand Down Expand Up @@ -442,3 +443,34 @@ pub async fn progress_order(
}
Ok(())
}

pub async fn fetch_and_cache_orders(
component: &str,
wfm_client: &wf_market::Client<wf_market::Authenticated>,
item_url: &str,
cache_path: Option<&Path>,
) -> Result<OrderList<OrderWithUser>, 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)
}
105 changes: 82 additions & 23 deletions src-tauri/src/live_scraper/modules/item.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use std::{
collections::HashSet,
path::PathBuf,
sync::{atomic::Ordering, Arc, Weak},
};

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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::<OrderList<OrderWithUser>>(&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
Expand Down Expand Up @@ -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 = {
Expand Down Expand Up @@ -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");
}
Expand Down
21 changes: 21 additions & 0 deletions src-tauri/utils/src/helper.rs
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,27 @@ pub fn read_json_file<T: serde::de::DeserializeOwned>(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<T: serde::Serialize>(
path: impl AsRef<std::path::Path>,
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<T> by multiple criteria using a predicate function
///
/// # Arguments
Expand Down
3 changes: 2 additions & 1 deletion src/components/DataDisplay/ChatMessage/ChatMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -42,7 +43,7 @@ export const ChatMessage = ({ user, msg, sender }: ChatMessageProps) => {
setOpen((o) => !o);
}}
>
{msg.raw_message}
{decodeHtmlEntities(msg.raw_message)}
</Alert>
</Group>
</Stack>
Expand Down
41 changes: 29 additions & 12 deletions src/pages/debug/tabs/debugging/index.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -23,17 +23,34 @@ export const DebuggingPanel = ({}: DebuggingPanelProps) => {

return (
<Box>
<DebuggingLiveItemEntryForm
onSubmit={async (values) => {
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);
}}
/>
<Group align="center">
<DebuggingLiveItemEntryForm
onSubmit={async (values) => {
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);
}}
/>
<Tooltip label={useTranslateTabDebugging("fake_orders_tooltip")}>
<Checkbox
label={useTranslateTabDebugging("fake_orders_label")}
checked={settings?.debugging.live_scraper.fake_orders || false}
size="sm"
onChange={async (e) => {
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);
}}
/>
</Tooltip>
</Group>
<DataTable
height={400}
className={`${classes.dataTableLogging}`}
Expand Down
9 changes: 5 additions & 4 deletions src/types/tauri.type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ export namespace TauriTypes {
export interface SettingsDebugging {
live_scraper: {
entries: DebuggingLiveItemEntry[];
fake_orders: boolean;
};
}
export interface ManualUpdate {
Expand Down Expand Up @@ -490,8 +491,8 @@ export namespace TauriTypes {
export interface ChartWithLabelsDto {
labels: Array<string>;
}
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[];
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -828,7 +829,7 @@ export namespace TauriTypes {
created_at: string;
properties: Record<string, any>;
}
export interface TradeEntryDetails extends TradeEntry { }
export interface TradeEntryDetails extends TradeEntry {}

export interface CreateTradeEntry {
raw: string;
Expand Down
10 changes: 10 additions & 0 deletions src/utils/helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down