raylu 19 часов назад
Родитель
Сommit
ae81458ded
6 измененных файлов с 180 добавлено и 1 удалено
  1. 1 0
      .gitignore
  2. 0 1
      ts/buy.ts
  3. 138 0
      ts/junk.ts
  4. 1 0
      www/index.html
  5. 32 0
      www/junk.html
  6. 8 0
      www/style.css

+ 1 - 0
.gitignore

@@ -6,6 +6,7 @@ uv.lock
 www/buy.js*
 www/corps.js*
 www/gov.js*
+www/junk.js*
 www/ledger.js*
 www/mat.js*
 www/plan.js*

+ 0 - 1
ts/buy.ts

@@ -21,7 +21,6 @@ const apiKey = document.querySelector('#api-key') as HTMLInputElement;
 document.querySelector('#fetch')!.addEventListener('click', async () => {
 	const supplyForDays = parseInt((document.querySelector('#days') as HTMLInputElement).value, 10);
 	const loader = document.querySelector('#loader') as HTMLElement;
-	loader.innerHTML = '';
 	loader.style.display = 'block';
 	try {
 		await calculate(username.value, apiKey.value, supplyForDays);

+ 138 - 0
ts/junk.ts

@@ -0,0 +1,138 @@
+import {cachedFetchJSON} from './cache';
+
+const username = document.querySelector('#username') as HTMLInputElement;
+const apiKey = document.querySelector('#api-key') as HTMLInputElement;
+const cxSelect = document.querySelector('#cx') as HTMLSelectElement;
+{
+	const storedUsername = localStorage.getItem('fio-username');
+	if (storedUsername)
+		username.value = storedUsername;
+	const storedApiKey = localStorage.getItem('fio-api-key');
+	if (storedApiKey)
+		apiKey.value = storedApiKey;
+}
+
+document.querySelector('#fetch')!.addEventListener('click', async () => {
+	const loader = document.querySelector('#loader') as HTMLElement;
+	loader.style.display = 'block';
+	try {
+		await calculate(username.value, apiKey.value, cxSelect.value);
+		localStorage.setItem('fio-username', username.value);
+		localStorage.setItem('fio-api-key', apiKey.value);
+	} catch (e) {
+		console.error(e);
+		(document.querySelector('#junk') as HTMLElement).innerText = e instanceof Error ? e.message : String(e);
+	}
+	loader.style.display = 'none';
+});
+
+const format = new Intl.NumberFormat(undefined, {maximumFractionDigits: 2}).format;
+
+async function calculate(username: string, apiKey: string, cx: string): Promise<void> {
+	const renderTarget = document.querySelector('#junk')!;
+	renderTarget.innerHTML = '';
+
+	const fioBurns: FIOBurn[] = await cachedFetchJSON('https://rest.fnar.net/fioweb/burn/user/' + username,
+		{headers: {'Authorization': apiKey}});
+	const totalConsumption = new Map<string, number>();
+	for (const fioBurn of fioBurns) {
+		const netConsumption = new Map<string, number>();
+		for (const item of fioBurn.OrderProduction)
+			netConsumption.set(item.MaterialTicker, -item.DailyAmount);
+		for (const item of [...fioBurn.OrderConsumption, ...fioBurn.WorkforceConsumption])
+			netConsumption.set(item.MaterialTicker, (netConsumption.get(item.MaterialTicker) ?? 0) + item.DailyAmount);
+		for (const [ticker, consumption] of netConsumption)
+			if (consumption > 0)
+				totalConsumption.set(ticker, (totalConsumption.get(ticker) ?? 0) + consumption);
+	}
+
+	const warehouse = await warehouseInventory(username, apiKey, cx);
+	const supplies = [...warehouse.entries()].map(([ticker, amount]) => {
+		const consumption = totalConsumption.get(ticker);
+		return {
+			ticker,
+			amount,
+			days: consumption === undefined || consumption <= 0 ? Number.POSITIVE_INFINITY : amount / consumption,
+			consumption: consumption ?? 0,
+		};
+	});
+	supplies.sort((a, b) => b.days - a.days);
+
+	const h2 = document.createElement('h2');
+	h2.textContent = 'warehouse supply';
+	renderTarget.appendChild(h2);
+
+	const table = document.createElement('table');
+	table.innerHTML = `
+		<thead>
+			<tr>
+				<th>mat</th>
+				<th>have</th>
+				<th>consumption/day</th>
+				<th>supply</th>
+			</tr>
+		</thead>`;
+	const tbody = document.createElement('tbody');
+	for (const supply of supplies) {
+		const tr = document.createElement('tr');
+		tr.innerHTML = `
+			<td>${supply.ticker}</td>
+			<td>${format(supply.amount)}</td>
+			<td>${format(supply.consumption)}</td>
+			<td>${Number.isFinite(supply.days) ? format(supply.days) + ' d' : '∞'}</td>
+		`;
+		tbody.appendChild(tr);
+	}
+	table.appendChild(tbody);
+	renderTarget.appendChild(table);
+}
+
+async function warehouseInventory(username: string, apiKey: string, cx: string): Promise<Map<string, number>> {
+	const warehouses: Warehouse[] = await cachedFetchJSON('https://rest.fnar.net/sites/warehouses/' + username,
+		{headers: {'Authorization': apiKey}});
+
+	const inventory = new Map<string, number>();
+	for (const warehouse of warehouses)
+		if (warehouse.LocationNaturalId === cx) {
+			const storage: Storage = await cachedFetchJSON(`https://rest.fnar.net/storage/${username}/${warehouse.StoreId}`,
+				{headers: {'Authorization': apiKey}});
+			for (const item of storage.StorageItems)
+				inventory.set(item.MaterialTicker, item.MaterialAmount);
+			break;
+		}
+	return inventory;
+}
+
+interface StorageItem {
+	MaterialTicker: string;
+	MaterialAmount: number;
+}
+
+interface Storage {
+	Name: string;
+	StorageItems: StorageItem[];
+	WeightLoad: number;
+	VolumeLoad: number;
+	Type: 'STORE' | 'WAREHOUSE_STORE' | 'FTL_FUEL_STORE' | 'STL_FUEL_STORE' | 'SHIP_STORE';
+}
+
+interface Warehouse {
+	StoreId: string;
+	LocationNaturalId: string;
+}
+
+interface Amount {
+	MaterialTicker: string;
+	DailyAmount: number;
+}
+
+interface FIOBurn {
+	PlanetId: string;
+	PlanetName: string;
+	PlanetNaturalId: string;
+	Error: any;
+	OrderConsumption: Amount[];
+	WorkforceConsumption: Amount[];
+	Inventory: StorageItem[];
+	OrderProduction: Amount[];
+}

+ 1 - 0
www/index.html

@@ -14,6 +14,7 @@
 		<p><a href="buy.html">buy</a></p>
 		<p><a href="mat.html">material</a></p>
 		<p><a href="corps.html">corporations</a></p>
+		<p><a href="junk.html">junk</a></p>
 		<p><a href="gov.html">government</a> — population calculator</p>
 		<p><a href="stats/">stats</a> — monthly stats without consumption (has produced/volume but not consumed/profit). <a href="https://git.raylu.net/raylu/prunstats">git.raylu.net/raylu/prunstats</a></p>
 	</main>

+ 32 - 0
www/junk.html

@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<html>
+<head>
+	<meta charset="UTF-8">
+	<title>PrUn junk finder</title>
+	<link rel="stylesheet" type="text/css" href="style.css">
+	<link rel="icon" href="https://www.raylu.net/hammer-man.svg" />
+	<meta name="viewport" content="width=device-width, initial-scale=1">
+	<meta name="theme-color" content="#222">
+</head>
+<body>
+	<a href="/">← back</a>
+	<main class="junk">
+		<form>
+			<label>FIO username: <input type="text" id="username"></label>
+			<label>FIO API key: <input type="password" size="30" id="api-key"></label>
+			<label>CX: <select id="cx">
+				<option value="ANT">AI1/ANT</option>
+				<option value="BEN">CI1/BEN</option>
+				<option value="ARC">CI2/ARC</option>
+				<option value="HRT" selected>IC1/HRT</option>
+				<option value="MOR">NC1/MOR</option>
+				<option value="HUB">NC2/HUB</option>
+			</select></label>
+			<input type="button" value="fetch" id="fetch">
+		</form>
+		<section id="loader"></section>
+		<section id="junk"></section>
+	</main>
+	<script src="junk.js"></script>
+</body>
+</html>

+ 8 - 0
www/style.css

@@ -150,6 +150,14 @@ main.gov {
 		color: #f80;
 	}
 }
+main.junk {
+	form {
+		label {
+			display: block;
+			margin-bottom: 0.5em;
+		}
+	}
+}
 
 main.kit {
 	width: 1200px;