diff --git a/index-en.php b/index-en.php
index 86a862e..30927b6 100644
--- a/index-en.php
+++ b/index-en.php
@@ -69,6 +69,233 @@ function index_vanilla_description_html(?string $description): string
return nl2br(htmlspecialchars($description, ENT_QUOTES, 'UTF-8'));
}
+function index_vanilla_uex_normalize_whitespace(string $value): string
+{
+ return trim((string) preg_replace('/\s+/u', ' ', html_entity_decode(strip_tags($value), ENT_QUOTES | ENT_HTML5, 'UTF-8')));
+}
+
+function index_vanilla_uex_normalize_search_text(string $value): string
+{
+ $value = function_exists('mb_strtolower')
+ ? mb_strtolower($value, 'UTF-8')
+ : strtolower($value);
+
+ $value = preg_replace('/[^[:alnum:]]+/u', ' ', $value);
+ return trim((string) preg_replace('/\s+/u', ' ', $value));
+}
+
+function index_vanilla_uex_title_matches_query(string $title, string $queryName): bool
+{
+ $normalizedTitle = index_vanilla_uex_normalize_search_text($title);
+ $normalizedQuery = index_vanilla_uex_normalize_search_text($queryName);
+
+ if ($normalizedTitle === '' || $normalizedQuery === '') {
+ return false;
+ }
+
+ if (strpos($normalizedTitle, $normalizedQuery) !== false) {
+ return true;
+ }
+
+ $queryTokens = array_values(array_filter(explode(' ', $normalizedQuery), static function (string $token): bool {
+ return preg_match('/\d/', $token) || strlen($token) >= 3;
+ }));
+
+ if ($queryTokens === []) {
+ return false;
+ }
+
+ foreach ($queryTokens as $token) {
+ if (strpos($normalizedTitle, $token) === false) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+function index_vanilla_uex_extract_price_value(string $rawPrice): ?int
+{
+ $rawPrice = trim($rawPrice);
+ if ($rawPrice === '') {
+ return null;
+ }
+
+ if (!preg_match('/([0-9][0-9\s,\.]*)((?:\s*[KMB])?)\s*(?:A?UEC)\b/i', $rawPrice, $matches)) {
+ return null;
+ }
+
+ $numberPart = preg_replace('/\s+/', '', (string) ($matches[1] ?? ''));
+ $suffix = strtoupper(trim((string) ($matches[2] ?? '')));
+
+ if ($numberPart === '') {
+ return null;
+ }
+
+ if ($suffix !== '') {
+ if (substr_count($numberPart, ',') === 1 && strpos($numberPart, '.') === false) {
+ $numberPart = str_replace(',', '.', $numberPart);
+ } else {
+ $numberPart = str_replace(',', '', $numberPart);
+ }
+ } else {
+ if (strpos($numberPart, ',') !== false && strpos($numberPart, '.') !== false) {
+ if (strrpos($numberPart, ',') > strrpos($numberPart, '.')) {
+ $numberPart = str_replace('.', '', $numberPart);
+ $numberPart = str_replace(',', '.', $numberPart);
+ } else {
+ $numberPart = str_replace(',', '', $numberPart);
+ }
+ } else {
+ $numberPart = str_replace(',', '', $numberPart);
+ }
+ }
+
+ if (!is_numeric($numberPart)) {
+ return null;
+ }
+
+ $value = (float) $numberPart;
+ $multiplier = match ($suffix) {
+ 'K' => 1000,
+ 'M' => 1000000,
+ 'B' => 1000000000,
+ default => 1,
+ };
+
+ return (int) round($value * $multiplier);
+}
+
+function index_vanilla_uex_parse_estimate_from_html(string $html, string $queryName, int $sampleLimit = 10): array
+{
+ $values = [];
+ $chunks = preg_split('/
]*>/i', $html) ?: [];
+
+ foreach ($chunks as $chunk) {
+ if (count($values) >= $sampleLimit) {
+ break;
+ }
+
+ if (!preg_match('/
]*class="text-bold"[^>]*>(.*?)<\/a>/is', $chunk, $titleMatches)) {
+ continue;
+ }
+
+ $title = index_vanilla_uex_normalize_whitespace((string) ($titleMatches[1] ?? ''));
+ if ($title === '' || !preg_match('/^WTS\b/i', $title) || !index_vanilla_uex_title_matches_query($title, $queryName)) {
+ continue;
+ }
+
+ if (!preg_match('/]*>(.*?)<\/h4>/is', $chunk, $priceMatches)) {
+ continue;
+ }
+
+ $priceValue = index_vanilla_uex_extract_price_value(index_vanilla_uex_normalize_whitespace((string) ($priceMatches[1] ?? '')));
+ if ($priceValue === null) {
+ continue;
+ }
+
+ $values[] = $priceValue;
+ }
+
+ if ($values === []) {
+ return [
+ 'has_estimate' => false,
+ 'average' => null,
+ 'formatted' => '—',
+ 'sample_count' => 0,
+ ];
+ }
+
+ $average = (int) round(array_sum($values) / count($values));
+
+ return [
+ 'has_estimate' => true,
+ 'average' => $average,
+ 'formatted' => '~' . number_format($average, 0, ',', ' ') . ' UEC',
+ 'sample_count' => count($values),
+ ];
+}
+
+function index_vanilla_uex_fetch_estimates(array $names, int $sampleLimit = 10): array
+{
+ $results = [];
+ $uniqueNames = [];
+
+ foreach ($names as $name) {
+ $name = trim((string) $name);
+ if ($name === '' || isset($uniqueNames[$name])) {
+ continue;
+ }
+
+ $uniqueNames[$name] = $name;
+ }
+
+ if ($uniqueNames === []) {
+ return $results;
+ }
+
+ $multiHandle = curl_multi_init();
+ $handles = [];
+ $userAgent = 'Mozilla/5.0 (compatible; FlatLogicVanillaDb/1.0; +https://uexcorp.space/)';
+
+ foreach ($uniqueNames as $name) {
+ $url = 'https://uexcorp.space/search?q=' . rawurlencode($name);
+ $handle = curl_init();
+ curl_setopt_array($handle, [
+ CURLOPT_URL => $url,
+ CURLOPT_RETURNTRANSFER => true,
+ CURLOPT_FOLLOWLOCATION => true,
+ CURLOPT_CONNECTTIMEOUT => 4,
+ CURLOPT_TIMEOUT => 8,
+ CURLOPT_USERAGENT => $userAgent,
+ CURLOPT_SSL_VERIFYPEER => true,
+ CURLOPT_SSL_VERIFYHOST => 2,
+ CURLOPT_HTTPHEADER => [
+ 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
+ 'Accept-Language: fr-FR,fr;q=0.9,en;q=0.8',
+ 'Cache-Control: no-cache',
+ ],
+ ]);
+
+ curl_multi_add_handle($multiHandle, $handle);
+ $handles[$name] = $handle;
+ }
+
+ $running = null;
+ do {
+ $status = curl_multi_exec($multiHandle, $running);
+ if ($running) {
+ curl_multi_select($multiHandle, 1.0);
+ }
+ } while ($running && $status === CURLM_OK);
+
+ foreach ($handles as $name => $handle) {
+ $error = curl_error($handle);
+ $httpCode = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE);
+ $body = (string) curl_multi_getcontent($handle);
+
+ if ($error !== '' || $httpCode < 200 || $httpCode >= 300 || trim($body) === '') {
+ $results[$name] = [
+ 'has_estimate' => false,
+ 'average' => null,
+ 'formatted' => 'Indisponible',
+ 'sample_count' => 0,
+ 'error' => true,
+ ];
+ } else {
+ $results[$name] = index_vanilla_uex_parse_estimate_from_html($body, $name, $sampleLimit);
+ $results[$name]['error'] = false;
+ }
+
+ curl_multi_remove_handle($multiHandle, $handle);
+ curl_close($handle);
+ }
+
+ curl_multi_close($multiHandle);
+
+ return $results;
+}
+
auth_start_session();
auth_bootstrap();
@@ -78,6 +305,34 @@ $is_authenticated = $session_cl_auth_user !== '';
$has_member_access = $is_authenticated && in_array($session_cl_auth_right, ['member', 'admin'], true);
$has_vanilla_db_access = $is_authenticated && in_array($session_cl_auth_right, ['member', 'moderator', 'admin'], true);
+if (isset($_GET['ajax']) && (string) $_GET['ajax'] === 'vanilla-price-estimates') {
+ header('Content-Type: application/json; charset=UTF-8');
+
+ if (!$has_vanilla_db_access) {
+ http_response_code(403);
+ echo json_encode([
+ 'ok' => false,
+ 'message' => 'Access denied.',
+ 'estimates' => new stdClass(),
+ ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
+ exit;
+ }
+
+ $requestedNames = $_GET['names'] ?? [];
+ if (!is_array($requestedNames)) {
+ $requestedNames = [$requestedNames];
+ }
+
+ $requestedNames = array_slice(array_values(array_filter(array_map('strval', $requestedNames), static fn ($name): bool => trim($name) !== '')), 0, 10);
+ $estimates = index_vanilla_uex_fetch_estimates($requestedNames, 10);
+
+ echo json_encode([
+ 'ok' => true,
+ 'estimates' => $estimates,
+ ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
+ exit;
+}
+
$scan_reference_rows = [];
$scan_reference_max_occurrence = 0;
$scan_reference_error = null;
@@ -97,7 +352,7 @@ $vanilla_db_current_page = 1;
$vanilla_db_result_start = 0;
$vanilla_db_result_end = 0;
$vanilla_db_base_query = ['open_modal' => 'vanilla-db'];
-$vanilla_db_reset_url = 'index.php?open_modal=vanilla-db';
+$vanilla_db_reset_url = 'index-en.php?open_modal=vanilla-db';
$should_open_vanilla_db_modal = false;
try {
@@ -302,7 +557,7 @@ if ($has_vanilla_db_access) {
if ($vanilla_db_search !== '') {
$vanilla_db_base_query['vanilla_search'] = $vanilla_db_search;
}
- $vanilla_db_reset_url = 'index.php?' . http_build_query(['open_modal' => 'vanilla-db']);
+ $vanilla_db_reset_url = 'index-en.php?' . http_build_query(['open_modal' => 'vanilla-db']);
try {
if (!isset($db) || !($db instanceof PDO)) {
@@ -1421,6 +1676,28 @@ if ($has_vanilla_db_access) {
word-break: break-all;
}
+ .vanilla-db-card-estimate {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.35rem;
+ font-size: 0.95em;
+ color: rgba(255, 224, 138, 0.92);
+ word-break: normal;
+ }
+
+ .vanilla-db-card-estimate strong {
+ font-weight: 600;
+ color: rgba(255, 236, 178, 0.98);
+ }
+
+ .vanilla-db-card-estimate-value.is-loading {
+ color: rgba(255, 255, 255, 0.62);
+ }
+
+ .vanilla-db-card-estimate-value.is-unavailable {
+ color: rgba(255, 180, 180, 0.88);
+ }
+
.vanilla-db-card-uuid {
display: inline-block;
font-size: 0.76em;
@@ -1961,7 +2238,7 @@ if ($has_vanilla_db_access) {
-