cacheKey($channel ?? $this->channel())); return is_string($value) && $value !== '' ? $value : null; } /** * Whether a newer release than the installed version is known from the cache (no network) — the * single source for the sidebar badge. A stale cache below the installed version never counts. */ public function updateAvailable(): bool { $latest = $this->cachedLatest(); if ($latest === null) { return false; } return version_compare(ltrim($latest, 'vV'), ltrim((string) config('clusev.version'), 'vV'), '>'); } /** * Force a fresh remote lookup, cache it, and return the newest tag for the channel. Null on any * failure (offline, non-2xx, malformed) so callers degrade gracefully. */ public function refresh(?string $channel = null): ?string { $channel ??= $this->channel(); $tag = $this->fetchRemoteLatestTag($channel); if ($tag !== null) { Cache::put($this->cacheKey($channel), $tag, now()->addMinutes(self::TTL_MINUTES)); } return $tag; } /** * Newest tag from the public Git host's API (Gitea: /api/v1/repos///tags). * Returns the version WITHOUT the leading "v" (e.g. "0.9.50"); null on any failure. */ public function fetchRemoteLatestTag(string $channel): ?string { $repo = (string) config('clusev.repository'); if (! preg_match('#^(https?://[^/]+)/([^/]+)/([^/]+?)(?:\.git)?/?$#', $repo, $m)) { return null; } [, $base, $owner, $name] = $m; try { $res = Http::timeout(5)->acceptJson() ->get("{$base}/api/v1/repos/{$owner}/{$name}/tags", ['limit' => 50]); if (! $res->successful()) { return null; } $names = array_map( static fn ($t): string => is_array($t) ? (string) ($t['name'] ?? '') : '', (array) $res->json(), ); return $this->newestVersion($names, $channel); } catch (\Throwable $e) { report($e); return null; } } /** * Highest version among the tag names, honouring the channel (stable rejects prereleases). * Returns the version without the "v" prefix. * * @param array $tagNames */ public function newestVersion(array $tagNames, string $channel): ?string { $pattern = $channel === 'beta' ? '/^v?\d+\.\d+\.\d+(?:-[0-9A-Za-z.]+)?$/' : '/^v?\d+\.\d+\.\d+$/'; $versions = []; foreach (array_unique($tagNames) as $tag) { $tag = trim((string) $tag); if ($tag !== '' && preg_match($pattern, $tag)) { $versions[] = ltrim($tag, 'vV'); } } if ($versions === []) { return null; } usort($versions, fn (string $a, string $b): int => version_compare($b, $a)); return $versions[0]; } }