diff --git a/README.md b/README.md deleted file mode 100644 index fa3005d..0000000 --- a/README.md +++ /dev/null @@ -1,180 +0,0 @@ -# voxlis API - -Roblox executor/version aggregator for Windows, macOS, Android, and iOS. It normalizes each service’s reported Roblox client version, validates it, and outputs a single JSON payload that’s easy to consume. - -

- PHP - API - License - Status -

- ---- - -## Features - -* PC baseline match (Windows/macOS): compares each service to Roblox LIVE CDN version. -* Mobile strictness (Android/iOS): marks a service updated only if Roblox’s mobile API returns `UpgradeAction === "None"` for that app build. -* Per‑version timing: tracks wait time until update; shows current elapsed or historical average in the output. -* Diagnostics: `?selftest=1`, `?diag=1` (mobile), `?diagpc=1` (PC). -* Caching & history: minimizes endpoint load and preserves recent timing data. - -> Note: timing values are measured in hours, not minutes. - ---- - -## Requirements - -* PHP 7.4+ (8.x OK) -* cURL extension enabled -* Outbound HTTPS to: `clientsettingscdn.roblox.com` and the listed service endpoints -* The web server user must be able to write to `./cache_v2/` - ---- - -## Quick Start - -1. Place `index.php` in a web-served directory. -2. Ensure `cache_v2/` is writable: - - ```bash - mkdir -p cache_v2 && chmod 775 cache_v2 - ``` -3. Fetch the default output: - - ```bash - curl -s 'https://your.host/index.php' | jq . - ``` -4. Run a self-test: - - ```bash - curl -s 'https://your.host/index.php?selftest=1' | jq . - ``` - ---- - -## Configuration - -| Constant | Default | Description | -| --------------- | -------------- | ------------------------------------------ | -| `UA` | Chrome-like UA | HTTP user-agent for requests | -| `HTTP_TIMEOUT` | 12 | Per-request timeout (seconds) | -| `CORS_ALLOW` | `*` | CORS allow-origin header | -| `CACHE_DIR_V2` | `./cache_v2` | Cache directory | -| `AGG_CACHE_TTL` | 1800 | Cache TTL for main payload (seconds) | -| `RBLX_VERS_TTL` | 1800 | TTL for Roblox baseline (seconds) | -| `DEBUG_MODE` | false | If false, serves cached payload when fresh | - ---- - -## Endpoints - -All are GET and return JSON. - -| Query | Description | -| --------------------- | ------------------------------------------------------------- | -| *(none)* | Aggregated results (cached unless `DEBUG_MODE=true`). | -| `?include_backends=1` | Adds detailed per-service backend status. | -| `?selftest=1` | Checks PHP, cURL, cache directory, and Roblox reachability. | -| `?diag=1` | Mobile diagnostics: extraction details and Roblox validation. | -| `?diagpc=1` | PC diagnostics: normalized versions vs Roblox baseline. | - ---- - -## How Roblox link checking works - -### Windows / macOS - -1. The script fetches Roblox’s official version info from: - - * `https://clientsettingscdn.roblox.com/v2/client-version/WindowsPlayer/channel/LIVE` - * `https://clientsettingscdn.roblox.com/v2/client-version/MacPlayer/channel/LIVE` -2. It extracts the `clientVersionUpload` value, which looks like `version-31fc142272764f02`. -3. Each executor’s endpoint is fetched, and a version-like string is extracted. -4. After normalization (removing prefixes, lowercasing), the script compares the value to the Roblox LIVE baseline. -5. If they match exactly, that service is marked as `updated: true`. - -### Android / iOS - -1. The script extracts a numeric version from each service, such as `2.695.960`. -2. It builds the Roblox app version identifier: - - * Android → `AppAndroidV2.695.960` - * iOS → `AppiOSv2.695.960` (note lowercase v) -3. It queries Roblox’s mobile client API: - - ``` - https://clientsettingscdn.roblox.com/v1/mobile-client-version?appVersion= - ``` -4. Roblox returns JSON like: - - ```json - { - "data": { "UpgradeAction": "None" } - } - ``` -5. If `UpgradeAction` equals `None`, the service is counted as updated. - ---- - -## Output Schema - -### Example (simplified) - -```json -{ - "windows": { - "zenith": {"updated": true, "version": "version-31fc142272764f02", "avg": {"version-31fc142272764f02": 47}} - }, - "RobloxVersions": { - "windows": "version-31fc142272764f02", - "macos": "version-7a1b3c4d5e6f7890", - "ios": ["version-2.695.956"], - "android": ["version-2.695.960"] - } -} -``` - -**Field meanings:** - -* `updated` – Whether the service matches Roblox (PC) or passes `UpgradeAction=None` (mobile). -* `version` – The extracted, normalized version string (always prefixed with `version-`). -* `avg` – The time (in hours) this version has been pending or historically averaged. - ---- - -## Services list - -Defined inline under `$SERVICES_INLINE` grouped by platform. Example: - -```php -"zenith" => [ - "url" => "https://zenith.win/api/v1/status", - "field" => "roblox_version" -] -``` - ---- - -## Caching - -* `cache_v2/versions_cache_v2.json` – main aggregated payload (30m TTL) -* `cache_v2/backends_status_v2.json` – backend debug info -* `cache_v2/History_v2.json` – version timing logs (max 10 waits per version) -* `cache.rblx_versions_v2.json` – Roblox baselines (30m TTL) - ---- - -## Troubleshooting - -* 500 or blank: check web server log and `php-error.log`. -* Roblox HTTP fails: outbound firewall or DNS issue. -* `unreachable`: endpoint offline. -* `no_version_reported`: specify a `field` path. -* Mobile always false: inspect `?diag=1` to see Roblox API response. - ---- - -## License - -MIT. See LICENSE. diff --git a/Tracker/list.php b/Tracker/list.php deleted file mode 100644 index e2c3824..0000000 --- a/Tracker/list.php +++ /dev/null @@ -1,691 +0,0 @@ - - * - iOS: AppiOSv (lowercase 'v') - * - Output per service: { "updated": , "version": "", "avg": } - * e.g. "avg": { "version-31fc142272764f02": 3 } ← HOURS (current elapsed if running, else historical average) - * - Per-version time logs: stores the last 10 waits (HOURS) for each version under History_v2.json -> _lagv - * - Diagnostics: ?selftest=1 • ?diag=1 (mobile) • ?diagpc=1 (PC) [optionally gated by ADMIN_TOKEN] - * - Caches in cache_v2/ to avoid conflicts. - */ - -ini_set('display_errors', '0'); // don't leak errors to clients -ini_set('log_errors', '1'); -ini_set('error_log', __DIR__ . '/php-error.log'); -error_reporting(E_ALL); - -if (!function_exists('str_starts_with')) { - function str_starts_with($haystack, $needle){ return $needle === '' || strpos($haystack, $needle) === 0; } -} - -/* ===== Config ===== */ -const UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'; -const HTTP_TIMEOUT = 12; -const HTTP_CONNECT_TIMEOUT = 6; -const HTTP_MAX_REDIRECTS = 3; - -const CORS_ALLOW = '*'; // set to your site if you want to restrict (e.g., 'https://example.com') - -const CACHE_DIR_V2 = __DIR__ . '/cache_v2'; -const AGG_CACHE_FILE_V2 = CACHE_DIR_V2 . '/versions_cache_v2.json'; -const AGG_CACHE_TTL = 1800; // 30m - -const BACKENDS_FILE_V2 = CACHE_DIR_V2 . '/backends_status_v2.json'; -const HIST_FILE_V2 = CACHE_DIR_V2 . '/History_v2.json'; -const HIST_MAX_ENTRIES = 200; - -const RBLX_VERS_FILE_V2 = __DIR__ . '/cache.rblx_versions_v2.json'; -const RBLX_VERS_TTL = 1800; - -// OFF in production if you want cache to serve aggressively -const DEBUG_MODE = false; - -// Optional: set an env var ADMIN_TOKEN to gate diagnostics & selftest endpoints -$ADMIN_TOKEN = getenv('ADMIN_TOKEN') ?: null; - -/* ===== Embedded endpoints (your list) ===== */ -$SERVICES_INLINE = [ - "windows" => [ - "charm" => ["url" => "https://charm.ong/info", "field" => "roblox_version"], - ], - "macos" => [ - // add macOS services here - ], - "android" => [ - "vegax" => ["url" => "https://gitlab.com/marsqq/vegax4/-/raw/main/version", "field" => null], - ], - "ios" => [ - "arceusx" => ["url" => "https://raw.githubusercontent.com/SPDM-Team/Arceus-X-NEO-public/refs/heads/main/Website/v5/ios.txt", "field" => null], - ], -]; - -/* ===== Fatal JSON handler (no path/line leak) ===== */ -register_shutdown_function(function () { - $e = error_get_last(); - if ($e && in_array($e['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR], true)) { - if (!headers_sent()) header('Content-Type: application/json'); - http_response_code(500); - // Keep detail in logs only; avoid leaking paths in responses. - echo json_encode(['error' => 'Fatal'], JSON_UNESCAPED_SLASHES); - } -}); - -/* ===== Utils & IO ===== */ -function cors(): void { header('Access-Control-Allow-Origin: ' . CORS_ALLOW); } -function enforce_get(): void { - $m = $_SERVER['REQUEST_METHOD'] ?? 'GET'; - if ($m !== 'GET' && $m !== 'HEAD') { header('Allow: GET, HEAD'); http_response_code(405); exit; } -} -function require_admin(?string $tokenFromEnv): void { - if ($tokenFromEnv === null) return; // no gating if no token set - $t = $_GET['token'] ?? ''; - if (!hash_equals($tokenFromEnv, $t)) { http_response_code(403); exit; } -} -function cache_put(string $file, $data): void { - $dir = dirname($file); - if (!is_dir($dir)) @mkdir($dir, 0775, true); - file_put_contents($file, is_string($data) ? $data : json_encode($data, JSON_UNESCAPED_SLASHES|JSON_PRETTY_PRINT)); -} -function cache_get(string $file, int $ttl, $default=null) { - if (!file_exists($file)) return $default; - if ($ttl > 0 && time() - filemtime($file) > $ttl) return $default; - $raw = (string)file_get_contents($file); - $j = json_decode($raw, true); - return $j === null ? $default : $j; -} -function send_json(int $code, array $payload): void { - header('Content-Type: application/json'); - http_response_code($code); - echo json_encode($payload, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT); - exit; -} -function send_formatted(int $code, array $payload): void { - header('Content-Type: application/json'); - http_response_code($code); - echo json_encode($payload, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT); - exit; -} - -/* ===== HTTP ===== */ -function http_json_or_text(string $url) { - if (!extension_loaded('curl')) return false; - $ch = curl_init($url); - curl_setopt_array($ch, [ - CURLOPT_RETURNTRANSFER => true, - CURLOPT_FOLLOWLOCATION => true, - CURLOPT_MAXREDIRS => HTTP_MAX_REDIRECTS, - CURLOPT_TIMEOUT => HTTP_TIMEOUT, - CURLOPT_CONNECTTIMEOUT => HTTP_CONNECT_TIMEOUT, - CURLOPT_SSL_VERIFYPEER => true, - CURLOPT_USERAGENT => UA, - ]); - $body = curl_exec($ch); - $code = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE); - curl_close($ch); - if ($body === false || $code < 200 || $code >= 400) return false; - $trim = trim((string)$body); - $j = json_decode($trim, true); - return (json_last_error() === JSON_ERROR_NONE) ? $j : $trim; -} - -/* ===== Roblox PC baselines (Windows/Mac) ===== */ -function rblx_versions(): array { - $cache = cache_get(RBLX_VERS_FILE_V2, RBLX_VERS_TTL, null); - if ($cache && isset($cache['latest'], $cache['_meta']['refreshedAt'])) return $cache; - - $get = function(string $platform){ return http_json_or_text("https://clientsettingscdn.roblox.com/v2/client-version/{$platform}/channel/LIVE"); }; - $out = ['latest'=>['windows'=>'','macos'=>''],'old'=>['windows'=>'','macos'=>''],'_meta'=>['refreshedAt'=>time()]]; - $w = $get('WindowsPlayer'); $m = $get('MacPlayer'); - - $toV = function($j){ - if (!is_array($j) || empty($j['clientVersionUpload'])) return ''; - $v = (string)$j['clientVersionUpload']; - return str_starts_with($v, 'version-') ? $v : ('version-'.$v); - }; - - $latestW = $toV($w) ?: ($cache['latest']['windows'] ?? ''); - $latestM = $toV($m) ?: ($cache['latest']['macos'] ?? ''); - - $out['old']['windows'] = $cache['latest']['windows'] ?? ''; - $out['old']['macos'] = $cache['latest']['macos'] ?? ''; - $out['latest']['windows'] = $latestW; - $out['latest']['macos'] = $latestM; - - cache_put(RBLX_VERS_FILE_V2, $out); - return $out; -} - -/* ===== Parsing helpers ===== */ -function normalize_version($v): string { - if (is_array($v)) { - foreach ($v as $item) { - $n = normalize_version($item); - if ($n !== '') return $n; - } - return ''; - } - $s = strtolower(trim((string)$v)); - if ($s === '') return ''; - // strip repeated "version-" tokens, then generic "v/ver/version" prefixes - $s = preg_replace('/^(?:version-)+/i', '', $s); - $s = preg_replace('/^(?:v|ver|version)[\s\-_]*/i', '', $s); - // cut trailing cruft if present - if (preg_match('/^([a-f0-9]{16})/i', $s, $m)) return strtolower($m[1]); // hash - if (preg_match('/^(\d+\.\d+\.\d+)/', $s, $m)) return $m[1]; // numeric - return trim($s); -} -function with_prefix_for_display(string $canonical): string { - if (str_starts_with($canonical, 'version-')) return $canonical; - if (preg_match('/^[a-f0-9]{16}$/i', $canonical) || preg_match('/^\d+\.\d+\.\d+$/', $canonical)) { - return 'version-'.$canonical; - } - return $canonical; -} -function dot_get(array $a, string $path) { - $cur = $a; - foreach (explode('.', $path) as $k) { - if (!is_array($cur) || !array_key_exists($k, $cur)) return null; - $cur = $cur[$k]; - } - return $cur; -} -function flatten_array($x): array { - $out = []; - $w = function($n) use (&$w,&$out){ if(is_array($n)) foreach($n as $v) $w($v); else $out[]=$n; }; - $w($x); return $out; -} - -/* Accept only version-looking values, ignore flags like "updating":1 */ -function is_version_like($x): bool { - if (!is_string($x) && !is_numeric($x)) return false; - $s = strtolower(trim((string)$x)); - if ($s === '') return false; - if (preg_match('/\bversion-[a-f0-9]{16}\b/i', $s)) return true; // Roblox hash form - if (preg_match('/\b\d+\.\d+\.\d+\b/', $s)) return true; // numeric semver-ish - return false; -} - -/** - * Smart extractor (hardened): - * - Honors dot field when provided (flattens nested structures) - * - Only returns version-like values - * - Ignores status/flag keys (e.g., "updating": 1) - * - If TEXT: only returns if it looks like a version - */ -function extract_versions($decoded, ?string $dotField) { - // 1) Honor explicit field path first - if ($dotField !== null && is_array($decoded)) { - $v = dot_get($decoded, $dotField); - if ($v !== null && !is_array($v)) return is_version_like($v) ? $v : ''; - if (is_array($v)) { - $flat = array_values(array_filter( - flatten_array($v), - fn($x)=> (is_string($x)||is_numeric($x)) && is_version_like($x) - )); - return $flat ? (count($flat)===1 ? $flat[0] : $flat) : ''; - } - } - - // 2) JSON objects: prefer Roblox-ish keys, ignore status/flag keys - if (is_array($decoded)) { - $preferKeys = [ - 'roblox','roblox_version','robloxversion', - 'supported_roblox_version','supportedclient','supportedclientversion', - 'client','clientversion','supportedclient','supportedclientversion' - ]; - $banKeys = ['updating','update','needsupdate','maintenance','status','online','ok','success','message','note','info']; - - $candidates = []; - $walker = function($node) use (&$walker,&$candidates,$preferKeys,$banKeys){ - if (!is_array($node)) return; - foreach ($node as $k=>$v) { - if (is_array($v)) { $walker($v); continue; } - if (!is_string($k)) continue; - $kl = strtolower($k); - if (in_array($kl, $banKeys, true)) continue; // skip flags like "updating" - if ((is_string($v) || is_numeric($v)) && is_version_like($v)) { - $candidates[] = [$kl, (string)$v]; - } - } - }; - $walker($decoded); - - if ($candidates) { - // try preferred keys first - foreach ($preferKeys as $want) { - foreach ($candidates as [$k,$v]) if ($k === $want) return $v; - } - // else first version-like value - return $candidates[0][1]; - } - - // fallback: scan all scalars but keep only version-like - $flat = array_values(array_filter( - flatten_array($decoded), - fn($x)=> (is_string($x)||is_numeric($x)) && is_version_like($x) - )); - return $flat ? (count($flat)===1 ? $flat[0] : $flat) : ''; - } - - // 3) Plain TEXT: only return if it looks like a version - if (is_string($decoded)) { - if (preg_match('/\bversion-[a-f0-9]{16}\b/i', $decoded, $m)) return $m[0]; - if (preg_match('/\b\d+\.\d+\.\d+\b/', $decoded, $m)) return 'version-'.$m[0]; - return ''; - } - - return ''; -} - -/* ===== History (version sightings for misc use) ===== */ -function hist_load(): array { return cache_get(HIST_FILE_V2, 0, []) ?: []; } -function hist_save(array $h): void { cache_put(HIST_FILE_V2, $h); } -function hist_add(array $h, string $platform, string $svc, string $version): array { - $ts = time(); - $h[$platform] = $h[$platform] ?? []; - $h[$platform][$svc] = $h[$platform][$svc] ?? []; - $ver = normalize_version($version) ?: $version; - $entries = $h[$platform][$svc]; - $last = end($entries); - if ($last === false || (!isset($last['version']) || $last['version'] !== $ver)) { - $entries[] = ['version'=>$ver,'ts'=>$ts]; - if (count($entries) > HIST_MAX_ENTRIES) $entries = array_slice($entries, -HIST_MAX_ENTRIES); - $h[$platform][$svc] = $entries; - } - return $h; -} - -/* ===== Per-version time-based tracking (HOURS to update) ===== - * Structure: - * _lagv[platform][svc][version] = { start: int|null, logs: int[] (<=10, HOURS) } - */ -function lagv_init(array &$h, string $platform, string $svc, string $version): void { - if (!isset($h['_lagv'])) $h['_lagv'] = []; - if (!isset($h['_lagv'][$platform])) $h['_lagv'][$platform] = []; - if (!isset($h['_lagv'][$platform][$svc])) $h['_lagv'][$platform][$svc] = []; - if (!isset($h['_lagv'][$platform][$svc][$version])) { - $h['_lagv'][$platform][$svc][$version] = ['start'=>null, 'logs'=>[]]; - } -} -/** Touch the timer: - * - NOT updated: ensure start time (set now if null) - * - UPDATED: if started, push elapsed hours to logs (cap 10) and clear start - */ -function lagv_touch(array $h, string $platform, string $svc, string $version, bool $updated, ?int $now=null): array { - $now = $now ?? time(); - lagv_init($h, $platform, $svc, $version); - - $start = $h['_lagv'][$platform][$svc][$version]['start']; - $logs = $h['_lagv'][$platform][$svc][$version]['logs']; - - if ($updated === false) { - if ($start === null) { - $h['_lagv'][$platform][$svc][$version]['start'] = $now; - } - } else { - if ($start !== null) { - // compute full hours elapsed (integer) - $hours = (int) floor(max(0, $now - (int)$start) / 3600); - if ($hours > 0) { - $logs[] = $hours; - if (count($logs) > 10) $logs = array_slice($logs, -10); - $h['_lagv'][$platform][$svc][$version]['logs'] = $logs; - } - $h['_lagv'][$platform][$svc][$version]['start'] = null; - } - } - return $h; -} - -/** Current elapsed HOURS (whole hours) for this version, else null */ -function lagv_current_hours(array $h, string $platform, string $svc, string $version, ?int $now=null): ?int { - $now = $now ?? time(); - $start = $h['_lagv'][$platform][$svc][$version]['start'] ?? null; - if ($start === null) return null; - return (int) floor(max(0, $now - (int)$start) / 3600); -} - -/** Historical average HOURS (integer) from logs */ -function lagv_avg_hours_from_logs(array $h, string $platform, string $svc, string $version): ?int { - $logs = $h['_lagv'][$platform][$svc][$version]['logs'] ?? []; - if (!$logs) return null; - return (int) round(array_sum($logs) / count($logs)); -} - -/* ===== Mobile validation: STRICT "None" ===== */ -function numeric_from_version($v): ?string { - if (!$v) return null; - $v = preg_replace('/^version-/', '', $v); - if ($v === null) return null; - return preg_match('/([0-9]+(?:\.[0-9]+)*)/', $v, $m) ? $m[1] : null; -} -/** true if UpgradeAction === "None"; false if different; null on error */ -function mobile_is_none(string $platform, string $versionMaybeWithPrefix): ?bool { - $num = numeric_from_version($versionMaybeWithPrefix); - if (!$num) return null; - $appVersion = $platform === 'ios' ? "AppiOSv{$num}" : ($platform === 'android' ? "AppAndroidV{$num}" : null); - if (!$appVersion) return null; - $url = 'https://clientsettingscdn.roblox.com/v1/mobile-client-version?appVersion=' . rawurlencode($appVersion); - $j = http_json_or_text($url); - if (!is_array($j)) return null; - $act = isset($j['data']['UpgradeAction']) ? (string)$j['data']['UpgradeAction'] : null; - if ($act === null) return null; - return strcasecmp($act, 'None') === 0; -} - -/* ===== Services loader ===== */ -function load_services(): array { - global $SERVICES_INLINE; - if (!is_array($SERVICES_INLINE) || empty($SERVICES_INLINE)) { - throw new RuntimeException('Embedded services are missing.'); - } - return $SERVICES_INLINE; -} - -/* ===== Misc helpers ===== */ -function any_matches($extracted, array $baselineSet): bool { - if (!$extracted || !$baselineSet) return false; - foreach ((array)flatten_array($extracted) as $v) { - $n = normalize_version($v); - if ($n !== '' && in_array($n, $baselineSet, true)) return true; - } - return false; -} -function detect_up($decoded): bool { - $positives = ['up','online','available','running','healthy','true','1','ok','active','enabled']; - $negatives = ['down','offline','unavailable','false','0','error','fail','failed','inactive','disabled']; - $hasT=false;$hasF=false; - $scan=function($n) use (&$scan,&$hasT,&$hasF,$positives,$negatives){ - if (is_array($n)) { foreach ($n as $v) $scan($v); return; } - $s = strtolower(trim((string)$n)); - if ($s==='') return; - if (in_array($s,$positives,true)) $hasT=true; - if (in_array($s,$negatives,true)) $hasF=true; - }; - $scan($decoded); - return !($hasF && !$hasT); -} - -/** - * Collect mobile versions that validate to UpgradeAction === "None". - * Returns: ['ios'=>['version-2.695.956', ...], 'android'=>['version-2.695.960', ...]] - */ -function compute_working_mobile_versions(array $services): array { - $out = ['ios'=>[], 'android'=>[]]; - foreach (['ios','android'] as $p) { - if (empty($services[$p]) || !is_array($services[$p])) continue; - $candidates = []; - foreach ($services[$p] as $svc => $cfg) { - $url = (string)($cfg['url'] ?? ''); - $field = array_key_exists('field', $cfg) ? (string)$cfg['field'] : null; - if ($url === '') continue; - $decoded = http_json_or_text($url); - if ($decoded === false) continue; - - $extracted = extract_versions($decoded, $field); - $canonical = is_scalar($extracted) ? (string)$extracted : (string)(normalize_version($extracted) ?: ''); - if ($canonical === '') continue; - - $candidateOut = with_prefix_for_display($canonical); - $ok = mobile_is_none($p, $canonical); // STRICT "None" - if ($ok === true) $candidates[] = $candidateOut; - } - $out[$p] = array_values(array_unique($candidates)); - } - return $out; -} - -/* ===== App ===== */ -try { - enforce_get(); - - // ---- Admin-gated diagnostics (set ADMIN_TOKEN in environment to enable) ---- - if (isset($_GET['selftest']) && $_GET['selftest'] === '1') { - require_admin($ADMIN_TOKEN); - cors(); - $checks = []; - $checks['php_version'] = PHP_VERSION; - $checks['curl_loaded'] = extension_loaded('curl'); - $checks['cache_v2_dir'] = [ - 'path' => CACHE_DIR_V2, - 'exists' => is_dir(CACHE_DIR_V2), - 'writable' => is_dir(CACHE_DIR_V2) ? is_writable(CACHE_DIR_V2) : is_writable(__DIR__), - ]; - $services = load_services(); - $checks['platform_keys'] = array_keys($services); - $ok = false; $httpNote = null; - if (extension_loaded('curl')) { - $ch = curl_init('https://clientsettingscdn.roblox.com/v1/ping'); - curl_setopt_array($ch, [ - CURLOPT_RETURNTRANSFER => true, - CURLOPT_FOLLOWLOCATION => true, - CURLOPT_TIMEOUT => 8, - CURLOPT_CONNECTTIMEOUT => 6, - CURLOPT_SSL_VERIFYPEER => true, - CURLOPT_USERAGENT => 'SelfTest' - ]); - $body = curl_exec($ch); - $code = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE); - $err = curl_error($ch); - curl_close($ch); - $ok = ($body !== false && $code >= 200 && $code < 400); - $httpNote = $ok ? "HTTP $code" : ($err ?: "HTTP $code"); - } - send_json(200, ['selftest' => $checks, 'roblox_http' => ['ok'=>$ok,'note'=>$httpNote]]); - } - - if (isset($_GET['diag']) && $_GET['diag'] === '1') { - require_admin($ADMIN_TOKEN); - cors(); - $services = load_services(); - $report = ['platforms' => array_keys($services), 'mobile' => ['android'=>[], 'ios'=>[]]]; - foreach (['android','ios'] as $p) { - if (empty($services[$p]) || !is_array($services[$p])) continue; - foreach ($services[$p] as $svc => $cfg) { - $url = (string)($cfg['url'] ?? ''); - $field = array_key_exists('field', $cfg) ? (string)$cfg['field'] : null; - $raw = $url ? http_json_or_text($url) : false; - $ex = $raw !== false ? extract_versions($raw, $field) : null; - $canon = is_scalar($ex) ? (string)$ex : (string)(normalize_version($ex) ?: ''); - $rep = $canon !== '' ? with_prefix_for_display($canon) : null; - $ok = $canon !== '' ? mobile_is_none($p, $canon) : null; - $report['mobile'][$p][$svc] = [ - 'url' => $url, - 'field' => $field, - 'extracted' => $ex, - 'canonical' => $canon, - 'reported_with_prefix' => $rep, - 'roblox_UpgradeAction_None' => $ok, - ]; - } - } - send_json(200, $report); - } - - if (isset($_GET['diagpc']) && $_GET['diagpc'] === '1') { - require_admin($ADMIN_TOKEN); - cors(); - $services = load_services(); - $rblx = rblx_versions(); - $base = [ - 'windows' => normalize_version($rblx['latest']['windows'] ?? ''), - 'macos' => normalize_version($rblx['latest']['macos'] ?? ''), - ]; - $out = ['baseline'=>$base, 'windows'=>[], 'macos'=>[]]; - foreach (['windows','macos'] as $p) { - foreach ($services[$p] ?? [] as $name => $cfg) { - $raw = http_json_or_text($cfg['url']); - $ex = $raw !== false ? extract_versions($raw, $cfg['field'] ?? null) : null; - $canon = is_scalar($ex) ? (string)$ex : (string)(normalize_version($ex) ?: ''); - $out[$p][$name] = [ - 'field' => $cfg['field'] ?? null, - 'raw_value' => $ex, - 'normalized'=> normalize_version($canon), - 'matches' => (normalize_version($canon) !== '' && normalize_version($canon) === $base[$p]), - ]; - } - } - send_json(200, $out); - } - // ---- End diagnostics ---- - - // Serve cache if allowed - if (!DEBUG_MODE) { - $cached = cache_get(AGG_CACHE_FILE_V2, AGG_CACHE_TTL, null); - if ($cached) { cors(); send_formatted(200, $cached); } - } - - // Normal execution - $rblx = rblx_versions(); // PC baselines - $services = load_services(); - $history = hist_load(); - - $simple = []; // platform => svc => ['updated'=>bool,'version'=>string|null,'avg'=>array|null] - $backends = []; // per-service debug/status - - foreach ($services as $platform => $list) { - $simple[$platform] = []; - $backends[$platform] = []; - - $pcBaseline = []; - if ($platform === 'windows' || $platform === 'macos') { - $b = isset($rblx['latest'][$platform]) ? $rblx['latest'][$platform] : ''; - if ($b) $pcBaseline = [ normalize_version($b) ]; - } - - foreach ($list as $svc => $cfg) { - $url = (string)($cfg['url'] ?? ''); - $field = array_key_exists('field', $cfg) ? (string)$cfg['field'] : null; - - if ($url === '') { - $simple[$platform][$svc] = ['updated'=>false,'version'=>null,'avg'=>null]; - $backends[$platform][$svc] = ['up'=>false,'version_match'=>false,'status'=>'unconfigured']; - continue; - } - - $decoded = http_json_or_text($url); - if ($decoded === false) { - $simple[$platform][$svc] = ['updated'=>false,'version'=>null,'avg'=>null]; - $backends[$platform][$svc] = ['up'=>false,'version_match'=>false,'status'=>'unreachable']; - continue; - } - - $extracted = extract_versions($decoded, $field); - $canonical = is_scalar($extracted) ? (string)$extracted : (string)(normalize_version($extracted) ?: ''); - $up = detect_up($decoded); - - if ($canonical !== '') $history = hist_add($history, $platform, $svc, $canonical); - - // Build a display version string - $displayVersion = $canonical !== '' ? with_prefix_for_display($canonical) : null; - - // MOBILE: strict "None" per-service - if ($platform === 'ios' || $platform === 'android') { - $okNone = $canonical !== '' ? mobile_is_none($platform, $canonical) : null; - $updated = ($okNone === true); // ONLY "None" counts as updated/working - - // Optional clarity when endpoints report no version (e.g., {"updating":1}) - $backends[$platform][$svc] = $backends[$platform][$svc] ?? []; - if ($canonical === '') { - $backends[$platform][$svc]['status'] = ($backends[$platform][$svc]['status'] ?? 'no_version_reported'); - } - - // Time-based tracking for current version - $avgField = null; - if ($displayVersion !== null) { - $history = lagv_touch($history, $platform, $svc, $displayVersion, $updated); - $cur = lagv_current_hours($history, $platform, $svc, $displayVersion); - if ($cur !== null) { - $avgField = [$displayVersion => $cur]; - } else { - $histAvg = lagv_avg_hours_from_logs($history, $platform, $svc, $displayVersion); - $avgField = ($histAvg === null) ? null : [$displayVersion => $histAvg]; - } - } - - $simple[$platform][$svc] = [ - 'updated' => $updated, - 'version' => $displayVersion, - 'avg' => $avgField, - ]; - $backends[$platform][$svc] = array_merge($backends[$platform][$svc], [ - 'up' => $up, - 'version_match' => $updated, - 'reported_version' => $displayVersion, - 'mobile_upgrade_action_none' => $okNone, - 'status' => ($backends[$platform][$svc]['status'] ?? ($updated - ? ($up ? 'ok' : 'version_ok_backend_down') - : ($up ? 'not_updated' : 'down'))), - ]); - continue; - } - - // PC: compare against Roblox CDN baseline - $match = $pcBaseline ? any_matches($extracted, $pcBaseline) : false; - $status = $match ? ($up ? 'ok' : 'version_ok_backend_down') : ($up ? 'not_updated' : 'down'); - - // Time-based tracking for current version - $avgField = null; - if ($displayVersion !== null) { - $history = lagv_touch($history, $platform, $svc, $displayVersion, $match); - $cur = lagv_current_hours($history, $platform, $svc, $displayVersion); - if ($cur !== null) { - $avgField = [$displayVersion => $cur]; - } else { - $histAvg = lagv_avg_hours_from_logs($history, $platform, $svc, $displayVersion); - $avgField = ($histAvg === null) ? null : [$displayVersion => $histAvg]; - } - } - - $simple[$platform][$svc] = [ - 'updated' => $match, - 'version' => $displayVersion, - 'avg' => $avgField, - ]; - $backends[$platform][$svc] = [ - 'up' => $up, - 'version_match' => $match, - 'status' => $status, - ]; - } - } - - // Save history after all mutations (includes _lagv) - hist_save($history); - - // RobloxVersions block (PC + mobile versions that are STRICT "None") - $workingMobile = compute_working_mobile_versions($services); - $RobloxVersions = [ - 'windows' => $rblx['latest']['windows'] ?? '', - 'macos' => $rblx['latest']['macos'] ?? '', - 'ios' => $workingMobile['ios'], // ONLY "None" - 'android' => $workingMobile['android'], // ONLY "None" - ]; - - $payload = $simple + ['RobloxVersions' => $RobloxVersions]; - - cache_put(AGG_CACHE_FILE_V2, $payload); - cache_put(BACKENDS_FILE_V2, ['backends'=>$backends,'timestamp'=>time()]); - - cors(); - if (!empty($_GET['include_backends']) && $_GET['include_backends'] === '1') { - send_json(200, ['results'=>$simple,'backends'=>$backends,'RobloxVersions'=>$RobloxVersions]); - } else { - send_formatted(200, $payload); - } - -} catch (Throwable $e) { - cors(); - $resp = ['error'=>'Internal Server Error']; - if (DEBUG_MODE) { - $resp += ['message'=>'(suppressed in prod)']; - } - send_json(500, $resp); -}