✨feature: Markets and shopping features
This commit is contained in:
635
all-markets.php
Normal file
635
all-markets.php
Normal file
@@ -0,0 +1,635 @@
|
||||
<?php
|
||||
/**
|
||||
* Spacetraders all-market probe scanner page.
|
||||
*
|
||||
* @package SpacetradersAPI
|
||||
* @author Keith Solomon <keith@keithsolomon.net>
|
||||
* @license MIT License
|
||||
* @version GIT: <git_id>
|
||||
* @link https://git.keithsolomon.net/keith/Spacetraders
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/lib/spacetraders-api.php';
|
||||
require_once __DIR__ . '/lib/spacetraders-storage.php';
|
||||
|
||||
/**
|
||||
* Resolve the active system symbol.
|
||||
*
|
||||
* @param array<int,array<string,mixed>> $ships Ship records.
|
||||
* @param array<string,mixed> $agent Agent record.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
function resolveCurrentSystemSymbol( array $ships, array $agent ): string {
|
||||
if (! empty( $ships ) ) {
|
||||
$shipSystem = (string) ( $ships[0]['nav']['systemSymbol'] ?? '' );
|
||||
if ($shipSystem !== '' ) {
|
||||
return $shipSystem;
|
||||
}
|
||||
}
|
||||
|
||||
$headquarters = (string) ( $agent['headquarters'] ?? '' );
|
||||
if ($headquarters === '' ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$hqParts = explode( '-', $headquarters );
|
||||
if (count( $hqParts ) < 2 ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return $hqParts[0] . '-' . $hqParts[1];
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect all marketplace waypoints in a system.
|
||||
*
|
||||
* @param SpacetradersApi $client API client.
|
||||
* @param string $systemSymbol System symbol.
|
||||
*
|
||||
* @return array<int,string>
|
||||
*/
|
||||
function collectMarketplaceWaypoints( SpacetradersApi $client, string $systemSymbol ): array {
|
||||
$waypoints = array();
|
||||
$page = 1;
|
||||
$total = 0;
|
||||
|
||||
do {
|
||||
$waypointsResponse = $client->listWaypoints(
|
||||
$systemSymbol,
|
||||
array(
|
||||
'page' => $page,
|
||||
'limit' => 20,
|
||||
)
|
||||
);
|
||||
|
||||
$pageData = $waypointsResponse['data'] ?? array();
|
||||
if (! is_array( $pageData ) || empty( $pageData ) ) {
|
||||
break;
|
||||
}
|
||||
|
||||
foreach ( $pageData as $waypoint ) {
|
||||
$waypointSymbol = (string) ( $waypoint['symbol'] ?? '' );
|
||||
if ($waypointSymbol === '' ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ( (array) ( $waypoint['traits'] ?? array() ) as $trait ) {
|
||||
if ((string) ( $trait['symbol'] ?? '' ) === 'MARKETPLACE' ) {
|
||||
$waypoints[] = $waypointSymbol;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$total = (int) ( $waypointsResponse['meta']['total'] ?? count( $waypoints ) );
|
||||
$page++;
|
||||
} while ( ( $page - 1 ) * 20 < $total );
|
||||
|
||||
sort( $waypoints );
|
||||
return array_values( array_unique( $waypoints ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Find candidate probe ships.
|
||||
*
|
||||
* @param array<int,array<string,mixed>> $ships Ship records.
|
||||
*
|
||||
* @return array<int,array<string,mixed>>
|
||||
*/
|
||||
function getProbeShips( array $ships ): array {
|
||||
$probeShips = array();
|
||||
|
||||
foreach ( $ships as $ship ) {
|
||||
$frameSymbol = strtoupper( (string) ( $ship['frame']['symbol'] ?? '' ) );
|
||||
$role = strtoupper( (string) ( $ship['registration']['role'] ?? '' ) );
|
||||
|
||||
if (str_contains( $frameSymbol, 'PROBE' ) || $role === 'SATELLITE' ) {
|
||||
$probeShips[] = $ship;
|
||||
}
|
||||
}
|
||||
|
||||
return $probeShips;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert market payload into export rows including prices.
|
||||
*
|
||||
* @param array<string,mixed> $marketData Market payload.
|
||||
*
|
||||
* @return array<int,array<string,mixed>>
|
||||
*/
|
||||
function buildExportsWithPrices( array $marketData ): array {
|
||||
$exports = (array) ( $marketData['exports'] ?? array() );
|
||||
$tradeGoods = (array) ( $marketData['tradeGoods'] ?? array() );
|
||||
$tradeGoodsBySymbol = array();
|
||||
|
||||
foreach ( $tradeGoods as $tradeGood ) {
|
||||
$symbol = (string) ( $tradeGood['symbol'] ?? '' );
|
||||
if ($symbol !== '' ) {
|
||||
$tradeGoodsBySymbol[ $symbol ] = $tradeGood;
|
||||
}
|
||||
}
|
||||
|
||||
$rows = array();
|
||||
foreach ( $exports as $export ) {
|
||||
$symbol = (string) ( $export['symbol'] ?? '' );
|
||||
if ($symbol === '' ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$tradeGood = (array) ( $tradeGoodsBySymbol[ $symbol ] ?? array() );
|
||||
$rows[] = array(
|
||||
'symbol' => $symbol,
|
||||
'purchasePrice' => (int) ( $tradeGood['purchasePrice'] ?? 0 ),
|
||||
'sellPrice' => (int) ( $tradeGood['sellPrice'] ?? 0 ),
|
||||
'supply' => (string) ( $tradeGood['supply'] ?? '' ),
|
||||
);
|
||||
}
|
||||
|
||||
usort(
|
||||
$rows,
|
||||
static function ( array $left, array $right ): int {
|
||||
return strcmp(
|
||||
(string) ( $left['symbol'] ?? '' ),
|
||||
(string) ( $right['symbol'] ?? '' )
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
return $rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan one full probe step: scan current market (if marketplace), then navigate to next target.
|
||||
*
|
||||
* @param SpacetradersApi $client API client.
|
||||
* @param SpacetradersStorage $storage Storage handler.
|
||||
* @param string $systemSymbol System symbol.
|
||||
* @param string $probeShipSymbol Probe ship symbol.
|
||||
* @param array<int,string> $marketWaypoints Marketplace waypoints.
|
||||
*
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
function runProbeMarketScanStep(
|
||||
SpacetradersApi $client,
|
||||
SpacetradersStorage $storage,
|
||||
string $systemSymbol,
|
||||
string $probeShipSymbol,
|
||||
array $marketWaypoints
|
||||
): array {
|
||||
$messages = array();
|
||||
|
||||
if ($probeShipSymbol === '' ) {
|
||||
return array(
|
||||
'messages' => array( 'No probe ship selected.' ),
|
||||
'error' => true,
|
||||
);
|
||||
}
|
||||
|
||||
if (empty( $marketWaypoints ) ) {
|
||||
return array(
|
||||
'messages' => array( 'No marketplace waypoints were found in this system.' ),
|
||||
'error' => true,
|
||||
);
|
||||
}
|
||||
|
||||
$probeResponse = $client->getShip( $probeShipSymbol );
|
||||
$probeData = (array) ( $probeResponse['data'] ?? array() );
|
||||
$shipStatus = (string) ( $probeData['nav']['status'] ?? '' );
|
||||
$currentWaypoint = (string) ( $probeData['nav']['waypointSymbol'] ?? '' );
|
||||
|
||||
if ($shipStatus === 'IN_TRANSIT' ) {
|
||||
$arrival = (string) ( $probeData['nav']['route']['arrival'] ?? '' );
|
||||
$messages[] = $arrival !== '' ?
|
||||
'Probe is already in transit. Arrival: ' . date( 'Y-m-d H:i:s', strtotime( $arrival ) ) . '.' :
|
||||
'Probe is already in transit.';
|
||||
return array(
|
||||
'messages' => $messages,
|
||||
'error' => false,
|
||||
);
|
||||
}
|
||||
|
||||
$currentIndex = array_search( $currentWaypoint, $marketWaypoints, true );
|
||||
|
||||
if ($currentIndex === false ) {
|
||||
$firstWaypoint = (string) ( $marketWaypoints[0] ?? '' );
|
||||
if ($firstWaypoint === '' ) {
|
||||
return array(
|
||||
'messages' => array( 'No marketplace waypoints were found in this system.' ),
|
||||
'error' => true,
|
||||
);
|
||||
}
|
||||
|
||||
if ($shipStatus === 'DOCKED' ) {
|
||||
$client->orbitShip( $probeShipSymbol );
|
||||
}
|
||||
|
||||
$client->navigateShip( $probeShipSymbol, $firstWaypoint );
|
||||
$storage->clearAllCache();
|
||||
$messages[] = 'Probe navigating to ' . $firstWaypoint . '. Run scan again after arrival.';
|
||||
|
||||
return array(
|
||||
'messages' => $messages,
|
||||
'error' => false,
|
||||
);
|
||||
}
|
||||
|
||||
if ($shipStatus !== 'DOCKED' ) {
|
||||
$client->dockShip( $probeShipSymbol );
|
||||
$shipStatus = 'DOCKED';
|
||||
}
|
||||
|
||||
try {
|
||||
$marketResponse = $client->getWaypointMarket( $systemSymbol, $currentWaypoint );
|
||||
$marketData = (array) ( $marketResponse['data'] ?? array() );
|
||||
$exports = buildExportsWithPrices( $marketData );
|
||||
|
||||
$storage->upsertMarketScanWaypoint(
|
||||
$systemSymbol,
|
||||
$currentWaypoint,
|
||||
$exports,
|
||||
$probeShipSymbol,
|
||||
''
|
||||
);
|
||||
|
||||
$messages[] = 'Scanned ' . $currentWaypoint . ' (' . count( $exports ) . ' exports).';
|
||||
} catch (SpacetradersApiException $e) {
|
||||
$storage->upsertMarketScanWaypoint(
|
||||
$systemSymbol,
|
||||
$currentWaypoint,
|
||||
array(),
|
||||
$probeShipSymbol,
|
||||
$e->getMessage()
|
||||
);
|
||||
$messages[] = 'Scan failed at ' . $currentWaypoint . ': ' . $e->getMessage();
|
||||
}
|
||||
|
||||
if (count( $marketWaypoints ) <= 1 ) {
|
||||
$messages[] = 'Scan cycle complete for all marketplace waypoints in ' . $systemSymbol . '.';
|
||||
return array(
|
||||
'messages' => $messages,
|
||||
'error' => false,
|
||||
);
|
||||
}
|
||||
|
||||
$nextIndex = ( (int) $currentIndex + 1 ) % count( $marketWaypoints );
|
||||
$nextWaypoint = (string) ( $marketWaypoints[ $nextIndex ] ?? '' );
|
||||
|
||||
if ($nextWaypoint !== '' && $nextWaypoint !== $currentWaypoint ) {
|
||||
if ($shipStatus === 'DOCKED' ) {
|
||||
$client->orbitShip( $probeShipSymbol );
|
||||
}
|
||||
|
||||
$client->navigateShip( $probeShipSymbol, $nextWaypoint );
|
||||
$storage->clearAllCache();
|
||||
$messages[] = 'Probe navigating to ' . $nextWaypoint . '. Run scan again after arrival.';
|
||||
} else {
|
||||
$messages[] = 'Scan cycle complete for all marketplace waypoints in ' . $systemSymbol . '.';
|
||||
}
|
||||
|
||||
return array(
|
||||
'messages' => $messages,
|
||||
'error' => false,
|
||||
);
|
||||
}
|
||||
|
||||
$config = require __DIR__ . '/lib/project-config.php';
|
||||
|
||||
$storage = new SpacetradersStorage( $config['db_path'] );
|
||||
$token = $storage->getAgentToken();
|
||||
|
||||
$statusMessages = array();
|
||||
$errorMessage = '';
|
||||
$agent = array();
|
||||
$ships = array();
|
||||
$probeShips = array();
|
||||
$selectedProbeSymbol = '';
|
||||
$currentSystemSymbol = '';
|
||||
$marketWaypoints = array();
|
||||
$scanRows = array();
|
||||
$itemSearch = trim( (string) ( $_GET['item'] ?? '' ) );
|
||||
|
||||
if (! is_string( $token ) || trim( $token ) === '' ) {
|
||||
$envToken = getenv( 'SPACETRADERS_TOKEN' );
|
||||
|
||||
if (is_string( $envToken ) && trim( $envToken ) !== '' ) {
|
||||
$token = trim( $envToken );
|
||||
$storage->setAgentToken( $token );
|
||||
}
|
||||
}
|
||||
|
||||
if (! is_string( $token ) || trim( $token ) === '' ) {
|
||||
$tokenError = 'No token found. Set one in config.php or SPACETRADERS_TOKEN.';
|
||||
}
|
||||
|
||||
if (! isset( $tokenError ) ) {
|
||||
$client = new SpacetradersApi(
|
||||
trim( $token ),
|
||||
$config['api_base_url'],
|
||||
(int) $config['api_timeout'],
|
||||
$storage,
|
||||
(int) $config['cache_ttl']
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
if (! isset( $tokenError ) ) {
|
||||
$agentResponse = $client->getMyAgent();
|
||||
$shipsResponse = $client->listMyShips();
|
||||
|
||||
$agent = $agentResponse['data'] ?? $agentResponse;
|
||||
$ships = $shipsResponse['data'] ?? $shipsResponse;
|
||||
$probeShips = getProbeShips( $ships );
|
||||
$currentSystemSymbol = resolveCurrentSystemSymbol( $ships, $agent );
|
||||
|
||||
foreach ( $probeShips as $probeShip ) {
|
||||
$probeSymbol = (string) ( $probeShip['symbol'] ?? '' );
|
||||
if ($probeSymbol !== '' ) {
|
||||
$selectedProbeSymbol = $probeSymbol;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (isset( $_POST['probe_ship_symbol'] ) ) {
|
||||
$postedProbeSymbol = trim( (string) $_POST['probe_ship_symbol'] );
|
||||
foreach ( $probeShips as $probeShip ) {
|
||||
if ((string) ( $probeShip['symbol'] ?? '' ) === $postedProbeSymbol ) {
|
||||
$selectedProbeSymbol = $postedProbeSymbol;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($currentSystemSymbol !== '' ) {
|
||||
$marketWaypoints = collectMarketplaceWaypoints( $client, $currentSystemSymbol );
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset( $_POST['run_probe_scan'] ) ) {
|
||||
$scanResult = runProbeMarketScanStep(
|
||||
$client,
|
||||
$storage,
|
||||
$currentSystemSymbol,
|
||||
$selectedProbeSymbol,
|
||||
$marketWaypoints
|
||||
);
|
||||
|
||||
foreach ( (array) ( $scanResult['messages'] ?? array() ) as $message ) {
|
||||
$statusMessages[] = (string) $message;
|
||||
}
|
||||
|
||||
if ((bool) ( $scanResult['error'] ?? false ) ) {
|
||||
$errorMessage = ! empty( $statusMessages ) ? $statusMessages[0] : 'Probe scan failed.';
|
||||
$statusMessages = array();
|
||||
} else {
|
||||
$shipsResponse = $client->listMyShips();
|
||||
$ships = $shipsResponse['data'] ?? $shipsResponse;
|
||||
$probeShips = getProbeShips( $ships );
|
||||
}
|
||||
}
|
||||
|
||||
if ($currentSystemSymbol !== '' ) {
|
||||
$scanRows = $storage->getMarketScanWaypointsBySystem( $currentSystemSymbol );
|
||||
}
|
||||
}
|
||||
} catch (SpacetradersApiException $e) {
|
||||
$errorMessage = $e->getMessage();
|
||||
}
|
||||
|
||||
$scanRowsByWaypoint = array();
|
||||
foreach ( $scanRows as $scanRow ) {
|
||||
$scanWaypointSymbol = (string) ( $scanRow['waypoint_symbol'] ?? '' );
|
||||
if ($scanWaypointSymbol !== '' ) {
|
||||
$scanRowsByWaypoint[ $scanWaypointSymbol ] = $scanRow;
|
||||
}
|
||||
}
|
||||
|
||||
$marketCards = array();
|
||||
foreach ( $marketWaypoints as $waypointSymbol ) {
|
||||
$scanRow = (array) ( $scanRowsByWaypoint[ $waypointSymbol ] ?? array() );
|
||||
$exports = (array) ( $scanRow['exports'] ?? array() );
|
||||
$matchesFilter = true;
|
||||
|
||||
if ($itemSearch !== '' ) {
|
||||
$matchesFilter = false;
|
||||
$needle = strtoupper( $itemSearch );
|
||||
|
||||
foreach ( $exports as $export ) {
|
||||
$symbol = strtoupper( (string) ( $export['symbol'] ?? '' ) );
|
||||
if ($symbol !== '' && str_contains( $symbol, $needle ) ) {
|
||||
$matchesFilter = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (! $matchesFilter ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$marketCards[] = array(
|
||||
'waypointSymbol' => $waypointSymbol,
|
||||
'exports' => $exports,
|
||||
'updatedAt' => (int) ( $scanRow['updated_at'] ?? 0 ),
|
||||
'error' => (string) ( $scanRow['error_message'] ?? '' ),
|
||||
);
|
||||
}
|
||||
|
||||
$selectedProbeShip = array();
|
||||
foreach ( $probeShips as $probeShip ) {
|
||||
if ((string) ( $probeShip['symbol'] ?? '' ) === $selectedProbeSymbol ) {
|
||||
$selectedProbeShip = $probeShip;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$selectedProbeStatus = (string) ( $selectedProbeShip['nav']['status'] ?? '' );
|
||||
$selectedProbeArrivalIso = (string) ( $selectedProbeShip['nav']['route']['arrival'] ?? '' );
|
||||
$selectedProbeWaypoint = (string) ( $selectedProbeShip['nav']['waypointSymbol'] ?? '' );
|
||||
?>
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Spacetraders - All Markets</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
</head>
|
||||
|
||||
<body class="container mx-auto px-4 py-8 bg-stone-800 text-gray-200">
|
||||
<?php require __DIR__ . '/main-menu.php'; ?>
|
||||
|
||||
<h1 class="text-3xl font-bold mb-3 underline decoration-gray-300 w-full"><a href="all-markets.php">Spacetraders - All Markets</a></h1>
|
||||
|
||||
<p class="mb-6">
|
||||
<a class="text-blue-300 hover:underline" href="market.php">Back to Markets</a>
|
||||
</p>
|
||||
|
||||
<?php if (isset( $tokenError ) ) : ?>
|
||||
<div class="mb-6 border border-red-500 p-4 rounded text-red-300">
|
||||
<?php echo htmlspecialchars( $tokenError ); ?>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
<?php exit; ?>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($errorMessage !== '' ) : ?>
|
||||
<div class="mb-6 border border-red-500 p-4 rounded text-red-300">
|
||||
<?php echo htmlspecialchars( $errorMessage ); ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (! empty( $statusMessages ) ) : ?>
|
||||
<div class="mb-6 border border-emerald-500 p-4 rounded text-emerald-300">
|
||||
<?php foreach ( $statusMessages as $statusMessage ) : ?>
|
||||
<p><?php echo htmlspecialchars( (string) $statusMessage ); ?></p>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="mb-6 border border-gray-600 p-4 rounded">
|
||||
<p><span class="font-bold">System:</span> <?php echo htmlspecialchars( $currentSystemSymbol !== '' ? $currentSystemSymbol : 'Unknown' ); ?></p>
|
||||
<p><span class="font-bold">Marketplace Waypoints:</span> <?php echo number_format( count( $marketWaypoints ) ); ?></p>
|
||||
<p><span class="font-bold">Probe Ships:</span> <?php echo number_format( count( $probeShips ) ); ?></p>
|
||||
</div>
|
||||
|
||||
<form method="post" class="mb-6 border border-gray-600 p-4 rounded flex flex-wrap items-end gap-3">
|
||||
<input type="hidden" name="run_probe_scan" value="1">
|
||||
<div>
|
||||
<label for="probe_ship_symbol" class="block text-sm mb-1">Probe Ship</label>
|
||||
<select id="probe_ship_symbol" name="probe_ship_symbol" class="px-3 py-2 rounded text-black min-w-72">
|
||||
<?php foreach ( $probeShips as $probeShip ) : ?>
|
||||
<?php
|
||||
$probeSymbol = (string) ( $probeShip['symbol'] ?? '' );
|
||||
$probeWaypoint = (string) ( $probeShip['nav']['waypointSymbol'] ?? '' );
|
||||
$probeStatus = (string) ( $probeShip['nav']['status'] ?? '' );
|
||||
?>
|
||||
<option value="<?php echo htmlspecialchars( $probeSymbol ); ?>" <?php echo $probeSymbol === $selectedProbeSymbol ? 'selected' : ''; ?>>
|
||||
<?php echo htmlspecialchars( $probeSymbol . ' @ ' . $probeWaypoint . ' (' . $probeStatus . ')' ); ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="px-4 py-2 bg-blue-700 rounded hover:bg-blue-600">Run Probe Scan Step</button>
|
||||
</form>
|
||||
|
||||
<?php if ($selectedProbeSymbol !== '' ) : ?>
|
||||
<div class="mb-6 border border-gray-600 p-4 rounded">
|
||||
<p><span class="font-bold">Selected Probe:</span> <?php echo htmlspecialchars( $selectedProbeSymbol ); ?></p>
|
||||
<p><span class="font-bold">Current Waypoint:</span> <?php echo htmlspecialchars( $selectedProbeWaypoint !== '' ? $selectedProbeWaypoint : 'Unknown' ); ?></p>
|
||||
<p>
|
||||
<span class="font-bold">Transit Timer:</span>
|
||||
<span
|
||||
class="probe-transit-timer"
|
||||
data-status="<?php echo htmlspecialchars( $selectedProbeStatus ); ?>"
|
||||
data-arrival="<?php echo htmlspecialchars( $selectedProbeArrivalIso ); ?>"
|
||||
></span>
|
||||
</p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<form method="get" class="mb-6 border border-gray-600 p-4 rounded flex flex-wrap items-end gap-3">
|
||||
<div>
|
||||
<label for="item" class="block text-sm mb-1">Search Export Item</label>
|
||||
<input id="item" name="item" type="text" value="<?php echo htmlspecialchars( $itemSearch ); ?>" placeholder="IRON_ORE" class="px-3 py-2 rounded text-black min-w-72">
|
||||
</div>
|
||||
<button type="submit" class="px-4 py-2 bg-indigo-700 rounded hover:bg-indigo-600">Search</button>
|
||||
<a href="all-markets.php" class="px-4 py-2 bg-gray-700 rounded hover:bg-gray-600">Clear</a>
|
||||
</form>
|
||||
|
||||
<?php if (empty( $marketCards ) ) : ?>
|
||||
<div class="border border-gray-600 p-4 rounded">
|
||||
No market cards match the current filters. Run a scan and/or broaden your search.
|
||||
</div>
|
||||
<?php else : ?>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
|
||||
<?php foreach ( $marketCards as $card ) : ?>
|
||||
<?php
|
||||
$cardWaypoint = (string) ( $card['waypointSymbol'] ?? '' );
|
||||
$cardExports = (array) ( $card['exports'] ?? array() );
|
||||
$cardUpdatedAt = (int) ( $card['updatedAt'] ?? 0 );
|
||||
$cardError = (string) ( $card['error'] ?? '' );
|
||||
?>
|
||||
<div class="border border-gray-600 rounded p-4 bg-stone-900/40">
|
||||
<h2 class="text-xl font-bold mb-2"><?php echo htmlspecialchars( $cardWaypoint ); ?></h2>
|
||||
<p class="text-sm text-gray-300 mb-3">
|
||||
Last Scan:
|
||||
<?php echo $cardUpdatedAt > 0 ? htmlspecialchars( date( 'Y-m-d H:i:s', $cardUpdatedAt ) ) : 'Not scanned yet'; ?>
|
||||
</p>
|
||||
|
||||
<?php if ($cardError !== '' ) : ?>
|
||||
<p class="text-red-300 text-sm mb-3"><?php echo htmlspecialchars( $cardError ); ?></p>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (empty( $cardExports ) ) : ?>
|
||||
<p class="text-gray-400 text-sm">No export price data yet.</p>
|
||||
<?php else : ?>
|
||||
<ul class="space-y-2 text-sm">
|
||||
<?php foreach ( $cardExports as $export ) : ?>
|
||||
<li class="border border-gray-700 rounded p-2">
|
||||
<p class="font-semibold"><?php echo htmlspecialchars( (string) ( formatString( $export['symbol'] ?? '' ) ) ); ?></p>
|
||||
<p class="text-gray-300">Buy: <?php echo number_format( (int) ( $export['purchasePrice'] ?? 0 ) ); ?></p>
|
||||
<p class="text-gray-300">Sell: <?php echo number_format( (int) ( $export['sellPrice'] ?? 0 ) ); ?></p>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
const timerNodes = document.querySelectorAll('.probe-transit-timer');
|
||||
|
||||
function formatTimer(seconds) {
|
||||
if (seconds <= 0) {
|
||||
return 'Arriving';
|
||||
}
|
||||
|
||||
const total = Math.max(0, Math.floor(seconds));
|
||||
const hours = Math.floor(total / 3600);
|
||||
const minutes = Math.floor((total % 3600) / 60);
|
||||
const secs = total % 60;
|
||||
|
||||
if (hours > 0) {
|
||||
return `${hours}:${String(minutes).padStart(2, '0')}:${String(secs).padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
return `${minutes}:${String(secs).padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
function renderTransitTimers() {
|
||||
const nowMs = Date.now();
|
||||
|
||||
timerNodes.forEach((node) => {
|
||||
const status = node.dataset.status || '';
|
||||
const arrival = node.dataset.arrival || '';
|
||||
|
||||
if (status !== 'IN_TRANSIT') {
|
||||
node.textContent = 'Ready';
|
||||
return;
|
||||
}
|
||||
|
||||
const arrivalMs = Date.parse(arrival);
|
||||
if (!Number.isFinite(arrivalMs)) {
|
||||
node.textContent = 'In transit';
|
||||
return;
|
||||
}
|
||||
|
||||
const remainingSeconds = Math.max(0, Math.floor((arrivalMs - nowMs) / 1000));
|
||||
node.textContent = formatTimer(remainingSeconds);
|
||||
});
|
||||
}
|
||||
|
||||
renderTransitTimers();
|
||||
if (timerNodes.length > 0) {
|
||||
window.setInterval(renderTransitTimers, 1000);
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -68,6 +68,23 @@ class SpacetradersStorage {
|
||||
'CREATE INDEX IF NOT EXISTS idx_api_cache_expires
|
||||
ON api_cache (expires_at)'
|
||||
);
|
||||
|
||||
$this->db->exec(
|
||||
'CREATE TABLE IF NOT EXISTS market_scan_waypoints (
|
||||
system_symbol TEXT NOT NULL,
|
||||
waypoint_symbol TEXT NOT NULL,
|
||||
exports_json TEXT NOT NULL,
|
||||
probe_ship_symbol TEXT NOT NULL,
|
||||
error_message TEXT NOT NULL,
|
||||
updated_at INTEGER NOT NULL,
|
||||
PRIMARY KEY (system_symbol, waypoint_symbol)
|
||||
)'
|
||||
);
|
||||
|
||||
$this->db->exec(
|
||||
'CREATE INDEX IF NOT EXISTS idx_market_scan_system_updated
|
||||
ON market_scan_waypoints (system_symbol, updated_at)'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -243,4 +260,99 @@ class SpacetradersStorage {
|
||||
public function clearAllCache(): void {
|
||||
$this->db->exec( 'DELETE FROM api_cache' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Store scanned export data for a marketplace waypoint.
|
||||
*
|
||||
* @param string $systemSymbol System symbol.
|
||||
* @param string $waypointSymbol Waypoint symbol.
|
||||
* @param array<int,array<string,mixed>> $exports Scanned export records.
|
||||
* @param string $probeShipSymbol Probe ship symbol used for scan.
|
||||
* @param string $errorMessage Optional scan error message.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function upsertMarketScanWaypoint(
|
||||
string $systemSymbol,
|
||||
string $waypointSymbol,
|
||||
array $exports,
|
||||
string $probeShipSymbol = '',
|
||||
string $errorMessage = ''
|
||||
): void {
|
||||
$exportsJson = json_encode( array_values( $exports ) );
|
||||
if ($exportsJson === false ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$stmt = $this->db->prepare(
|
||||
'INSERT INTO market_scan_waypoints (
|
||||
system_symbol,
|
||||
waypoint_symbol,
|
||||
exports_json,
|
||||
probe_ship_symbol,
|
||||
error_message,
|
||||
updated_at
|
||||
)
|
||||
VALUES (
|
||||
:system_symbol,
|
||||
:waypoint_symbol,
|
||||
:exports_json,
|
||||
:probe_ship_symbol,
|
||||
:error_message,
|
||||
:updated_at
|
||||
)
|
||||
ON CONFLICT(system_symbol, waypoint_symbol) DO UPDATE SET
|
||||
exports_json = excluded.exports_json,
|
||||
probe_ship_symbol = excluded.probe_ship_symbol,
|
||||
error_message = excluded.error_message,
|
||||
updated_at = excluded.updated_at'
|
||||
);
|
||||
|
||||
$stmt->execute(
|
||||
array(
|
||||
':system_symbol' => $systemSymbol,
|
||||
':waypoint_symbol' => $waypointSymbol,
|
||||
':exports_json' => $exportsJson,
|
||||
':probe_ship_symbol' => $probeShipSymbol,
|
||||
':error_message' => $errorMessage,
|
||||
':updated_at' => time(),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load scanned market export data for a system.
|
||||
*
|
||||
* @param string $systemSymbol System symbol.
|
||||
*
|
||||
* @return array<int,array<string,mixed>>
|
||||
*/
|
||||
public function getMarketScanWaypointsBySystem( string $systemSymbol ): array {
|
||||
$stmt = $this->db->prepare(
|
||||
'SELECT
|
||||
system_symbol,
|
||||
waypoint_symbol,
|
||||
exports_json,
|
||||
probe_ship_symbol,
|
||||
error_message,
|
||||
updated_at
|
||||
FROM market_scan_waypoints
|
||||
WHERE system_symbol = :system_symbol
|
||||
ORDER BY waypoint_symbol ASC'
|
||||
);
|
||||
$stmt->execute( array( ':system_symbol' => $systemSymbol ) );
|
||||
$rows = $stmt->fetchAll();
|
||||
|
||||
if (! is_array( $rows ) ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
foreach ( $rows as &$row ) {
|
||||
$decodedExports = json_decode( (string) ( $row['exports_json'] ?? '[]' ), true );
|
||||
$row['exports'] = is_array( $decodedExports ) ? $decodedExports : array();
|
||||
}
|
||||
unset( $row );
|
||||
|
||||
return $rows;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,6 +68,8 @@ try {
|
||||
<span class="mx-2">|</span>
|
||||
<a href="market.php" class="text-blue-400 hover:underline">Markets</a>
|
||||
<span class="mx-2">|</span>
|
||||
<a href="all-markets.php" class="text-blue-400 hover:underline">All Markets</a>
|
||||
<span class="mx-2">|</span>
|
||||
<a href="buy-ships.php" class="text-blue-400 hover:underline">Buy Ships</a>
|
||||
<span class="mx-2">|</span>
|
||||
<a href="mining-fleet.php" class="text-blue-400 hover:underline">Mining Fleet</a>
|
||||
|
||||
@@ -137,6 +137,10 @@ try {
|
||||
|
||||
<h1 class="text-3xl font-bold mb-6 underline decoration-gray-300 w-full"><a href="market.php">Spacetraders - Markets</a></h1>
|
||||
|
||||
<p class="mb-6">
|
||||
<a class="text-blue-300 hover:underline" href="all-markets.php">View All Markets</a>
|
||||
</p>
|
||||
|
||||
<?php if (isset( $tokenError ) ) : ?>
|
||||
<div class="mb-6 border border-red-500 p-4 rounded text-red-300">
|
||||
<?php echo htmlspecialchars( $tokenError ); ?>
|
||||
|
||||
Reference in New Issue
Block a user