Kaynağa Gözat

Add_Persistent_LocalStorage_State

Implemented automated client-side persistence for all UI configurations.

        PURPOSE:
        To fulfill a user request for persistent state tracking across page reloads. Previously, all active dropdown selections, volume filters, and dynamic column sorts were lost whenever the frontend was refreshed, causing UX friction. By storing the configuration variables in the browser's `localStorage` cache, the application immediately resumes exactly where the user left off.

        IMPLEMENTATION DETAILS:
        - Added a `saveState()` function that maps all active UI values (DOM properties and internal `MetricType`/sort state trackers) into explicitly named string keys within `localStorage`.
        - Hooked `saveState()` into the very end of the `render()` execution block. Because `render()` is triggered sequentially by every single event listener in the file, placing the hook here centralizes the tracking logic and guarantees that only fully-validated, successfully rendered configurations are committed to disk (e.g. invalid combinations like selecting a building outside of the active expertise filter self-heal to empty strings).
        - Created a State Initialization block executed immediately upon script entry. This overrides default assignments with truthy checks against `localStorage`, populating dropdowns prior to the initial visual rendering.
        - Orchestrated a temporary `savedBuilding` injection pass to ensure that dynamically generated dropdown target values are restored correctly.
Thomas Knott 2 hafta önce
ebeveyn
işleme
71b22d2b35
7 değiştirilmiş dosya ile 1733 ekleme ve 640 silme
  1. 49 18
      ts/roi.ts
  2. 61 17
      www/roi.js
  3. 0 0
      www/roi.js.map
  4. 399 148
      www/roi_ai1.json
  5. 402 150
      www/roi_ci1.json
  6. 420 157
      www/roi_ic1.json
  7. 402 150
      www/roi_nc1.json

+ 49 - 18
ts/roi.ts

@@ -8,6 +8,9 @@ async function getROI(cx: string) {
 	return {lastModified, profits};
 }
 
+// EXTREME DETAIL: Hoisted the MetricType so it can be utilized by the state initialization block below.
+type MetricType = 'vwap' | 'bid' | 'ask';
+
 const lowVolume = document.querySelector('input#low-volume') as HTMLInputElement;
 
 const cxSelect = document.querySelector('select#cx') as HTMLSelectElement;
@@ -35,16 +38,25 @@ const formatDecimal = new Intl.NumberFormat(undefined,
 		{maximumFractionDigits: 2, maximumSignificantDigits: 6, roundingPriority: 'lessPrecision'}).format;
 const formatWhole = new Intl.NumberFormat(undefined, {maximumFractionDigits: 0}).format;
 
-let currentSortKey: keyof ProfitWithMetrics | 'outputs' = 'break_even';
-let currentSortAsc: boolean = true;
+// EXTREME DETAIL: --- STATE INITIALIZATION ---
+// Upon script execution (page load), we immediately query the browser's localStorage.
+// If valid keys exist, we override the default DOM selections with the persisted state.
+if (localStorage.getItem('roi-cx')) cxSelect.value = localStorage.getItem('roi-cx')!;
+if (localStorage.getItem('roi-expertise')) expertiseSelect.value = localStorage.getItem('roi-expertise')!;
+if (localStorage.getItem('roi-low-volume')) lowVolume.checked = localStorage.getItem('roi-low-volume') === 'true';
+
+// The building options haven't been dynamically generated yet, so we store the target in a temporary variable.
+let savedBuilding = localStorage.getItem('roi-building') || '';
+
+let currentSortKey: keyof ProfitWithMetrics | 'outputs' = (localStorage.getItem('roi-sort-key') as any) || 'break_even';
+let currentSortAsc: boolean = localStorage.getItem('roi-sort-asc') !== 'false'; // Defaults to true if null
 let headersInitialized = false;
 let metricControlsInitialized = false;
 
-// EXTREME DETAIL: Global state trackers to determine which pricing metric is applied to which column mathematically.
-type MetricType = 'vwap' | 'bid' | 'ask';
-let capexMetric: MetricType = 'vwap';
-let opexMetric: MetricType = 'vwap';
-let revenueMetric: MetricType = 'vwap';
+let capexMetric: MetricType = (localStorage.getItem('roi-capex-metric') as MetricType) || 'vwap';
+let opexMetric: MetricType = (localStorage.getItem('roi-opex-metric') as MetricType) || 'vwap';
+let revenueMetric: MetricType = (localStorage.getItem('roi-revenue-metric') as MetricType) || 'vwap';
+// ------------------------------------------
 
 async function render() {
 	const tbody = document.querySelector('tbody')!;
@@ -55,8 +67,6 @@ async function render() {
 		roiCache[cx] = getROI(cx);
 	const {lastModified, profits} = await roiCache[cx];
 
-	// EXTREME DETAIL: We dynamically inject three select dropdowns prior to rendering the table.
-	// We bind event listeners to them to update the global MetricType states and force a re-render.
 	if (!metricControlsInitialized) {
 		const controls = document.createElement('div');
 		controls.style.marginBottom = '15px';
@@ -74,6 +84,11 @@ async function render() {
 		const table = document.querySelector('table');
 		if (table) table.parentNode?.insertBefore(controls, table);
 
+        // EXTREME DETAIL: Apply the stored state defaults to the newly generated dropdown nodes.
+		(document.getElementById('capex-metric') as HTMLSelectElement).value = capexMetric;
+		(document.getElementById('opex-metric') as HTMLSelectElement).value = opexMetric;
+		(document.getElementById('revenue-metric') as HTMLSelectElement).value = revenueMetric;
+
 		document.getElementById('capex-metric')!.addEventListener('change', (e) => {
 			capexMetric = (e.target as HTMLSelectElement).value as MetricType;
 			render();
@@ -91,7 +106,6 @@ async function render() {
 
 	if (!headersInitialized) {
 		const ths = document.querySelectorAll('th');
-		// Note that 'capex' and 'cost_per_day' here map dynamically to the newly derived states later in the file.
 		const keys: (keyof ProfitWithMetrics | 'outputs')[] = [
 			'outputs', 'expertise', 'profit_per_area', 'break_even', 
 			'capex_val', 'opex_val', 'logistics_per_area', 'market_capacity_area'
@@ -119,7 +133,9 @@ async function render() {
 	const buildings: {ticker: string, expertise: keyof typeof expertise}[] = Array.from(buildingTickers)
 			.map((building) => ({ticker: building, expertise: profits.find(p => p.building === building)!.expertise}))
 			.sort((a, b) => a.ticker.localeCompare(b.ticker));
-	let selectedBuilding = buildingSelect.value;
+    
+    // EXTREME DETAIL: Inject the stored 'savedBuilding' target if this is the first execution.
+	let selectedBuilding = buildingSelect.value || savedBuilding;
 	let buildingFound = false;
 	buildingSelect.innerHTML = '<option value="">(all)</option>';
 	for (const building of buildings)
@@ -135,6 +151,9 @@ async function render() {
 		}
 	if (!buildingFound)
 		selectedBuilding = '';
+    
+    // Clear the injection buffer so future renders correctly rely purely on DOM/User manipulation.
+    savedBuilding = ''; 
 
 	const filteredProfits = profits.filter(p => {
 		const volumeRatio = p.output_per_day / p.average_traded_7d;
@@ -144,9 +163,6 @@ async function render() {
 		return true;
 	});
 
-	// EXTREME DETAIL: We map over the filtered array to compute the final derivation.
-	// By executing this map BEFORE the sort algorithm runs, the columns dynamically organize themselves
-	// perfectly around whichever permutations the user selected in the dropdowns.
 	const profitsWithMetrics: ProfitWithMetrics[] = filteredProfits.map(p => {
 		const capex_val = p.capex[capexMetric];
 		const opex_val = p.opex[opexMetric];
@@ -202,6 +218,25 @@ async function render() {
 
 		tbody.appendChild(tr);
 	}
+	document.getElementById('last-updated')!.textContent =
+		`last updated: ${lastModified.toLocaleString(undefined, {dateStyle: 'full', timeStyle: 'long', hour12: false})}`;
+
+    // EXTREME DETAIL: By calling saveState() at the very conclusion of the render loop,
+    // we take a final snapshot of the fully sanitized UI state (ensuring we don't accidentally
+    // save invalid building + expertise configurations to the hard drive).
+    saveState();
+}
+
+function saveState() {
+	localStorage.setItem('roi-cx', cxSelect.value);
+	localStorage.setItem('roi-expertise', expertiseSelect.value);
+	localStorage.setItem('roi-building', buildingSelect.value);
+	localStorage.setItem('roi-low-volume', lowVolume.checked.toString());
+	localStorage.setItem('roi-sort-key', currentSortKey);
+	localStorage.setItem('roi-sort-asc', currentSortAsc.toString());
+	localStorage.setItem('roi-capex-metric', capexMetric);
+	localStorage.setItem('roi-opex-metric', opexMetric);
+	localStorage.setItem('roi-revenue-metric', revenueMetric);
 }
 
 function color(n: number, low: number, high: number): string {
@@ -209,9 +244,6 @@ function color(n: number, low: number, high: number): string {
 	return `color-mix(in xyz, #0aa ${scale * 100}%, #f80)`;
 }
 
-// EXTREME DETAIL: formatMatPrices relies on evaluating the user's selected metric ('vwap', 'bid', or 'ask').
-// To avoid math crashes, if a user requests a Bid/Ask display for an item lacking that specific liquidity,
-// the fallback logic defaults to the standard 7-day VWAP.
 function formatMatPrices(matPrices: MatPrice[], metric: MetricType, runs_per_day: number): string {
 	return matPrices.map(({ticker, amount, vwap_7d, bid, ask}) => {
 		const val = metric === 'vwap' ? vwap_7d : metric === 'bid' ? (bid ?? vwap_7d) : (ask ?? vwap_7d);
@@ -250,7 +282,6 @@ interface Profit {
 	market_capacity_area: number
 }
 
-// Interface extension utilized for the dynamic frontend mapping array
 interface ProfitWithMetrics extends Profit {
 	capex_val: number;
 	opex_val: number;

+ 61 - 17
www/roi.js

@@ -53,6 +53,10 @@ var formatWhole = new Intl.NumberFormat(undefined, { maximumFractionDigits: 0 })
 var currentSortKey = "break_even";
 var currentSortAsc = true;
 var headersInitialized = false;
+var metricControlsInitialized = false;
+var capexMetric = "vwap";
+var opexMetric = "vwap";
+var revenueMetric = "vwap";
 async function render() {
   const tbody = document.querySelector("tbody");
   tbody.innerHTML = "";
@@ -60,6 +64,37 @@ async function render() {
   if (!roiCache[cx])
     roiCache[cx] = getROI(cx);
   const { lastModified, profits } = await roiCache[cx];
+  if (!metricControlsInitialized) {
+    const controls = document.createElement("div");
+    controls.style.marginBottom = "15px";
+    controls.innerHTML = `
+			<label style="margin-right: 15px;">CapEx Price: 
+				<select id="capex-metric"><option value="vwap">VWAP</option><option value="bid">Bid</option><option value="ask">Ask</option></select>
+			</label>
+			<label style="margin-right: 15px;">OpEx Price: 
+				<select id="opex-metric"><option value="vwap">VWAP</option><option value="bid">Bid</option><option value="ask">Ask</option></select>
+			</label>
+			<label>Revenue (Outputs) Price: 
+				<select id="revenue-metric"><option value="vwap">VWAP</option><option value="bid">Bid</option><option value="ask">Ask</option></select>
+			</label>
+		`;
+    const table = document.querySelector("table");
+    if (table)
+      table.parentNode?.insertBefore(controls, table);
+    document.getElementById("capex-metric").addEventListener("change", (e) => {
+      capexMetric = e.target.value;
+      render();
+    });
+    document.getElementById("opex-metric").addEventListener("change", (e) => {
+      opexMetric = e.target.value;
+      render();
+    });
+    document.getElementById("revenue-metric").addEventListener("change", (e) => {
+      revenueMetric = e.target.value;
+      render();
+    });
+    metricControlsInitialized = true;
+  }
   if (!headersInitialized) {
     const ths = document.querySelectorAll("th");
     const keys = [
@@ -67,8 +102,8 @@ async function render() {
       "expertise",
       "profit_per_area",
       "break_even",
-      "capex",
-      "cost_per_day",
+      "capex_val",
+      "opex_val",
       "logistics_per_area",
       "market_capacity_area"
     ];
@@ -117,7 +152,16 @@ async function render() {
       return false;
     return true;
   });
-  filteredProfits.sort((a, b) => {
+  const profitsWithMetrics = filteredProfits.map((p) => {
+    const capex_val = p.capex[capexMetric];
+    const opex_val = p.opex[opexMetric];
+    const revenue_val = p.revenue[revenueMetric];
+    const profit_per_day = revenue_val - opex_val;
+    const profit_per_area = profit_per_day / p.area;
+    const break_even = profit_per_day > 0 ? (capex_val + 3 * opex_val) / profit_per_day : Infinity;
+    return { ...p, capex_val, opex_val, revenue_val, profit_per_day, profit_per_area, break_even };
+  });
+  profitsWithMetrics.sort((a, b) => {
     let valA = a[currentSortKey];
     let valB = b[currentSortKey];
     if (currentSortKey === "outputs") {
@@ -130,42 +174,42 @@ async function render() {
       return currentSortAsc ? 1 : -1;
     return 0;
   });
-  for (const p of filteredProfits) {
+  for (const p of profitsWithMetrics) {
+    const volumeRatio = p.output_per_day / p.average_traded_7d;
     const tr = document.createElement("tr");
     tr.innerHTML = `
 			<td>${p.outputs.map((o) => o.ticker).join(", ")}</td>
 			<td>${expertise[p.expertise]}</td>
 			<td style="color: ${color(p.profit_per_area, 0, 300)}">${formatDecimal(p.profit_per_area)}</td>
 			<td><span style="color: ${color(p.break_even, 30, 3)}">${formatDecimal(p.break_even)}</span>d</td>
-			<td style="color: ${color(p.capex, 300000, 40000)}">${formatWhole(p.capex)}</td>
-			<td style="color: ${color(p.cost_per_day, 40000, 1000)}">${formatWhole(p.cost_per_day)}</td>
+			<td style="color: ${color(p.capex_val, 300000, 40000)}">${formatWhole(p.capex_val)}</td>
+			<td style="color: ${color(p.opex_val, 40000, 1000)}">${formatWhole(p.opex_val)}</td>
 			<td style="color: ${color(p.logistics_per_area, 2, 0.2)}">${formatDecimal(p.logistics_per_area)}</td>
 			<td style="color: ${color(p.market_capacity_area, 20, 500)}">${formatWhole(p.market_capacity_area)}</td>
 		`;
     const output = tr.querySelector("td");
     output.dataset.tooltip = p.recipe;
     const profitCell = tr.querySelectorAll("td")[2];
-    const revenue = p.outputs.reduce((sum, o) => sum + o.amount * o.vwap_7d, 0);
-    const inputCost = p.input_costs.reduce((sum, o) => sum + o.amount * o.vwap_7d, 0);
-    profitCell.dataset.tooltip = formatMatPrices(p.outputs) + `
-
-` + formatMatPrices(p.input_costs) + `
-` + "worker consumables: " + formatWhole(p.worker_consumable_cost_per_day) + `
+    profitCell.dataset.tooltip = formatMatPrices(p.outputs, revenueMetric, p.runs_per_day) + `
 
-` + `(${formatWhole(revenue)} - ${formatWhole(inputCost)}) × ${formatDecimal(p.runs_per_day)} runs ` + `- ${formatWhole(p.worker_consumable_cost_per_day)} = ${formatDecimal(p.profit_per_day)}
+` + formatMatPrices(p.input_costs, opexMetric, p.runs_per_day) + `
+` + `(${formatWhole(p.revenue_val)} - ${formatWhole(p.opex_val)}) = ${formatDecimal(p.profit_per_day)}
 ` + `${formatDecimal(p.profit_per_day)} / ${formatWhole(p.area)} area = ${formatDecimal(p.profit_per_area)}`;
     const marketCell = tr.querySelectorAll("td")[7];
     marketCell.dataset.tooltip = `Market Capacity: ${formatWhole(p.average_traded_7d)} traded/day ÷ ${formatDecimal(p.output_per_day / p.area)} produced/day/area = ${formatWhole(p.market_capacity_area)} equivalent areas`;
     tbody.appendChild(tr);
   }
-  document.getElementById("last-updated").textContent = `last updated: ${lastModified.toLocaleString(undefined, { dateStyle: "full", timeStyle: "long", hour12: false })}`;
 }
 function color(n, low, high) {
   const scale = Math.min(Math.max((n - low) / (high - low), 0), 1);
   return `color-mix(in xyz, #0aa ${scale * 100}%, #f80)`;
 }
-function formatMatPrices(matPrices) {
-  return matPrices.map(({ ticker, amount, vwap_7d }) => `${ticker}: ${amount} × ${formatDecimal(vwap_7d)} = ${formatWhole(amount * vwap_7d)}`).join(`
+function formatMatPrices(matPrices, metric, runs_per_day) {
+  return matPrices.map(({ ticker, amount, vwap_7d, bid, ask }) => {
+    const val = metric === "vwap" ? vwap_7d : metric === "bid" ? bid ?? vwap_7d : ask ?? vwap_7d;
+    const daily_amount = amount * runs_per_day;
+    return `${ticker}: ${formatDecimal(daily_amount)} × ${formatDecimal(val)} = ${formatWhole(daily_amount * val)}`;
+  }).join(`
 `);
 }
 setupPopover();
@@ -175,4 +219,4 @@ expertiseSelect.addEventListener("change", render);
 buildingSelect.addEventListener("change", render);
 render();
 
-//# debugId=360E975B4A4AE32464756E2164756E21
+//# debugId=96EE1F33EAAE56CC64756E2164756E21

Dosya farkı çok büyük olduğundan ihmal edildi
+ 0 - 0
www/roi.js.map


Dosya farkı çok büyük olduğundan ihmal edildi
+ 399 - 148
www/roi_ai1.json


Dosya farkı çok büyük olduğundan ihmal edildi
+ 402 - 150
www/roi_ci1.json


Dosya farkı çok büyük olduğundan ihmal edildi
+ 420 - 157
www/roi_ic1.json


Dosya farkı çok büyük olduğundan ihmal edildi
+ 402 - 150
www/roi_nc1.json


Bu fark içinde çok fazla dosya değişikliği olduğu için bazı dosyalar gösterilmiyor