|
|
@@ -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;
|