diff --git a/app/Jobs/CheckVersionUpdateJob.php b/app/Jobs/CheckVersionUpdateJob.php
new file mode 100644
index 0000000..0794688
--- /dev/null
+++ b/app/Jobs/CheckVersionUpdateJob.php
@@ -0,0 +1,216 @@
+errorResponse();
+ }
+
+ $backoffUntil = Cache::get(self::BACKOFF_KEY);
+ if ($this->isInBackoffPeriod($backoffUntil)) {
+ return $this->rateLimitResponse($backoffUntil);
+ }
+
+ $cachedResponse = Cache::get(self::CACHE_KEY);
+ $response = $this->fetchOrUseCache($cachedResponse, $updateSettings->prereleases, $backoffUntil);
+
+ if (! $response) {
+ return $this->errorResponse('fetch_failed');
+ }
+
+ [$latestVersion, $releaseData] = $this->extractLatestVersion($response, $updateSettings->prereleases);
+ $isNewer = $latestVersion && version_compare($latestVersion, $currentVersion, '>');
+
+ return [
+ 'latest_version' => $latestVersion,
+ 'is_newer' => $isNewer,
+ 'release_data' => $releaseData,
+ 'error' => null,
+ ];
+ } catch (ConnectionException $e) {
+ Log::error('Version check failed: '.$e->getMessage());
+
+ return $this->errorResponse('connection_failed');
+ } catch (Exception $e) {
+ Log::error('Unexpected error in version check: '.$e->getMessage());
+
+ return $this->errorResponse('unexpected_error');
+ }
+ }
+
+ private function isInBackoffPeriod(?\Illuminate\Support\Carbon $backoffUntil): bool
+ {
+ return $backoffUntil !== null && now()->isBefore($backoffUntil);
+ }
+
+ private function rateLimitResponse(\Illuminate\Support\Carbon $backoffUntil): array
+ {
+ return [
+ 'latest_version' => null,
+ 'is_newer' => false,
+ 'release_data' => null,
+ 'error' => 'rate_limit',
+ 'backoff_until' => $backoffUntil->timestamp,
+ ];
+ }
+
+ private function errorResponse(?string $error = null): array
+ {
+ return [
+ 'latest_version' => null,
+ 'is_newer' => false,
+ 'release_data' => null,
+ 'error' => $error,
+ ];
+ }
+
+ private function fetchOrUseCache(?array $cachedResponse, bool $enablePrereleases, ?\Illuminate\Support\Carbon $backoffUntil): ?array
+ {
+ if ($cachedResponse && ! $this->forceRefresh) {
+ return $cachedResponse;
+ }
+
+ if ($this->isInBackoffPeriod($backoffUntil)) {
+ return $cachedResponse;
+ }
+
+ try {
+ $httpResponse = $this->fetchReleases($enablePrereleases);
+
+ if ($httpResponse->status() === 429) {
+ return $this->handleRateLimit($cachedResponse);
+ }
+
+ if ($httpResponse->successful()) {
+ $responseData = $httpResponse->json();
+ Cache::put(self::CACHE_KEY, $responseData, self::CACHE_TTL);
+
+ return $responseData;
+ }
+
+ Log::warning('GitHub API request failed', [
+ 'status' => $httpResponse->status(),
+ 'body' => $httpResponse->body(),
+ ]);
+
+ return $cachedResponse;
+ } catch (ConnectionException $e) {
+ Log::debug('Failed to fetch releases: '.$e->getMessage());
+
+ return $cachedResponse ?? null;
+ } catch (Exception $e) {
+ Log::debug('Failed to fetch releases: '.$e->getMessage());
+
+ return $cachedResponse ?? null;
+ }
+ }
+
+ private function fetchReleases(bool $enablePrereleases)
+ {
+ $githubRepo = config('app.github_repo');
+ $apiBaseUrl = "https://api.github.com/repos/{$githubRepo}";
+ $endpoint = $enablePrereleases ? "{$apiBaseUrl}/releases" : "{$apiBaseUrl}/releases/latest";
+
+ return Http::timeout(10)->connectTimeout(5)->get($endpoint);
+ }
+
+ private function handleRateLimit(?array $cachedResponse): ?array
+ {
+ $backoffUntil = now()->addMinutes(self::BACKOFF_MINUTES);
+ Cache::put(self::BACKOFF_KEY, $backoffUntil, 600);
+ Log::warning('GitHub API rate limit exceeded. Backing off for 10 minutes.');
+
+ return $cachedResponse;
+ }
+
+ private function extractLatestVersion(array $response, bool $enablePrereleases): array
+ {
+ if (! $enablePrereleases || ! is_array($response) || ! isset($response[0])) {
+ return [
+ Arr::get($response, 'tag_name'),
+ $response,
+ ];
+ }
+
+ [$stableRelease, $prerelease] = $this->findReleases($response);
+
+ if ($prerelease && $stableRelease) {
+ $prereleaseVersion = Arr::get($prerelease, 'tag_name');
+ $stableVersion = Arr::get($stableRelease, 'tag_name');
+
+ if (version_compare($prereleaseVersion, $stableVersion, '>')) {
+ return [$prereleaseVersion, $prerelease];
+ }
+
+ return [$stableVersion, $stableRelease];
+ }
+
+ if ($prerelease) {
+ return [Arr::get($prerelease, 'tag_name'), $prerelease];
+ }
+
+ if ($stableRelease) {
+ return [Arr::get($stableRelease, 'tag_name'), $stableRelease];
+ }
+
+ return [null, null];
+ }
+
+ private function findReleases(array $allReleases): array
+ {
+ $stableRelease = null;
+ $prerelease = null;
+
+ foreach ($allReleases as $release) {
+ $tagName = Arr::get($release, 'tag_name');
+ if (! $tagName) {
+ continue;
+ }
+
+ $isPrerelease = (bool) Arr::get($release, 'prerelease', false);
+
+ if ($isPrerelease && ! $prerelease) {
+ $prerelease = $release;
+ } elseif (! $isPrerelease && ! $stableRelease) {
+ $stableRelease = $release;
+ }
+
+ if ($stableRelease && $prerelease) {
+ break;
+ }
+ }
+
+ return [$stableRelease, $prerelease];
+ }
+}
diff --git a/app/Settings/UpdateSettings.php b/app/Settings/UpdateSettings.php
new file mode 100644
index 0000000..d3ab45c
--- /dev/null
+++ b/app/Settings/UpdateSettings.php
@@ -0,0 +1,15 @@
+=13"
+ },
+ "require-dev": {
+ "doctrine/coding-standard": "^9 || ^12 || ^13",
+ "phpstan/phpstan": "1.4.10 || 2.1.11",
+ "phpstan/phpstan-phpunit": "^1.0 || ^2",
+ "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6 || ^10.5 || ^11.5 || ^12",
+ "psr/log": "^1 || ^2 || ^3"
+ },
+ "suggest": {
+ "psr/log": "Allows logging deprecations via PSR-3 logger implementation"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Doctrine\\Deprecations\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "A small layer on top of trigger_error(E_USER_DEPRECATED) or PSR-3 logging with options to disable all deprecations or selectively for packages.",
+ "homepage": "https://www.doctrine-project.org/",
+ "support": {
+ "issues": "https://github.com/doctrine/deprecations/issues",
+ "source": "https://github.com/doctrine/deprecations/tree/1.1.5"
+ },
+ "time": "2025-04-07T20:06:18+00:00"
+ },
{
"name": "doctrine/inflector",
"version": "2.1.0",
@@ -4041,6 +4089,117 @@
},
"time": "2020-10-15T08:29:30+00:00"
},
+ {
+ "name": "phpdocumentor/reflection-common",
+ "version": "2.2.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phpDocumentor/ReflectionCommon.git",
+ "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b",
+ "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.2 || ^8.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-2.x": "2.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "phpDocumentor\\Reflection\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Jaap van Otterdijk",
+ "email": "opensource@ijaap.nl"
+ }
+ ],
+ "description": "Common reflection classes used by phpdocumentor to reflect the code structure",
+ "homepage": "http://www.phpdoc.org",
+ "keywords": [
+ "FQSEN",
+ "phpDocumentor",
+ "phpdoc",
+ "reflection",
+ "static analysis"
+ ],
+ "support": {
+ "issues": "https://github.com/phpDocumentor/ReflectionCommon/issues",
+ "source": "https://github.com/phpDocumentor/ReflectionCommon/tree/2.x"
+ },
+ "time": "2020-06-27T09:03:43+00:00"
+ },
+ {
+ "name": "phpdocumentor/type-resolver",
+ "version": "1.12.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phpDocumentor/TypeResolver.git",
+ "reference": "92a98ada2b93d9b201a613cb5a33584dde25f195"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/92a98ada2b93d9b201a613cb5a33584dde25f195",
+ "reference": "92a98ada2b93d9b201a613cb5a33584dde25f195",
+ "shasum": ""
+ },
+ "require": {
+ "doctrine/deprecations": "^1.0",
+ "php": "^7.3 || ^8.0",
+ "phpdocumentor/reflection-common": "^2.0",
+ "phpstan/phpdoc-parser": "^1.18|^2.0"
+ },
+ "require-dev": {
+ "ext-tokenizer": "*",
+ "phpbench/phpbench": "^1.2",
+ "phpstan/extension-installer": "^1.1",
+ "phpstan/phpstan": "^1.8",
+ "phpstan/phpstan-phpunit": "^1.1",
+ "phpunit/phpunit": "^9.5",
+ "rector/rector": "^0.13.9",
+ "vimeo/psalm": "^4.25"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-1.x": "1.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "phpDocumentor\\Reflection\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Mike van Riel",
+ "email": "me@mikevanriel.com"
+ }
+ ],
+ "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names",
+ "support": {
+ "issues": "https://github.com/phpDocumentor/TypeResolver/issues",
+ "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.12.0"
+ },
+ "time": "2025-11-21T15:09:14+00:00"
+ },
{
"name": "phpoption/phpoption",
"version": "1.9.5",
@@ -4226,6 +4385,53 @@
],
"time": "2025-12-15T11:51:42+00:00"
},
+ {
+ "name": "phpstan/phpdoc-parser",
+ "version": "2.3.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phpstan/phpdoc-parser.git",
+ "reference": "16dbf9937da8d4528ceb2145c9c7c0bd29e26374"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/16dbf9937da8d4528ceb2145c9c7c0bd29e26374",
+ "reference": "16dbf9937da8d4528ceb2145c9c7c0bd29e26374",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.4 || ^8.0"
+ },
+ "require-dev": {
+ "doctrine/annotations": "^2.0",
+ "nikic/php-parser": "^5.3.0",
+ "php-parallel-lint/php-parallel-lint": "^1.2",
+ "phpstan/extension-installer": "^1.0",
+ "phpstan/phpstan": "^2.0",
+ "phpstan/phpstan-phpunit": "^2.0",
+ "phpstan/phpstan-strict-rules": "^2.0",
+ "phpunit/phpunit": "^9.6",
+ "symfony/process": "^5.2"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "PHPStan\\PhpDocParser\\": [
+ "src/"
+ ]
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "PHPDoc parser with support for nullable, intersection and generic types",
+ "support": {
+ "issues": "https://github.com/phpstan/phpdoc-parser/issues",
+ "source": "https://github.com/phpstan/phpdoc-parser/tree/2.3.1"
+ },
+ "time": "2026-01-12T11:33:04+00:00"
+ },
{
"name": "pragmarx/google2fa",
"version": "v9.0.0",
@@ -5096,6 +5302,91 @@
],
"time": "2025-07-17T15:46:43+00:00"
},
+ {
+ "name": "spatie/laravel-settings",
+ "version": "3.6.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/spatie/laravel-settings.git",
+ "reference": "fae93dadb8f748628ecaf5710f494adf790255b2"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/spatie/laravel-settings/zipball/fae93dadb8f748628ecaf5710f494adf790255b2",
+ "reference": "fae93dadb8f748628ecaf5710f494adf790255b2",
+ "shasum": ""
+ },
+ "require": {
+ "ext-json": "*",
+ "illuminate/database": "^11.0|^12.0",
+ "php": "^8.2",
+ "phpdocumentor/type-resolver": "^1.5",
+ "spatie/temporary-directory": "^1.3|^2.0"
+ },
+ "require-dev": {
+ "ext-redis": "*",
+ "mockery/mockery": "^1.4",
+ "orchestra/testbench": "^9.0|^10.0",
+ "pestphp/pest": "^2.0|^3.0",
+ "pestphp/pest-plugin-laravel": "^2.0|^3.0",
+ "phpstan/extension-installer": "^1.1",
+ "phpstan/phpstan-deprecation-rules": "^1.0",
+ "phpstan/phpstan-phpunit": "^1.0",
+ "spatie/laravel-data": "^2.0.0|^4.0.0",
+ "spatie/pest-plugin-snapshots": "^2.0",
+ "spatie/phpunit-snapshot-assertions": "^4.2|^5.0",
+ "spatie/ray": "^1.36"
+ },
+ "suggest": {
+ "spatie/data-transfer-object": "Allows for DTO casting to settings. (deprecated)"
+ },
+ "type": "library",
+ "extra": {
+ "laravel": {
+ "providers": [
+ "Spatie\\LaravelSettings\\LaravelSettingsServiceProvider"
+ ]
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Spatie\\LaravelSettings\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Ruben Van Assche",
+ "email": "ruben@spatie.be",
+ "homepage": "https://spatie.be",
+ "role": "Developer"
+ }
+ ],
+ "description": "Store your application settings",
+ "homepage": "https://github.com/spatie/laravel-settings",
+ "keywords": [
+ "laravel-settings",
+ "spatie"
+ ],
+ "support": {
+ "issues": "https://github.com/spatie/laravel-settings/issues",
+ "source": "https://github.com/spatie/laravel-settings/tree/3.6.0"
+ },
+ "funding": [
+ {
+ "url": "https://spatie.be/open-source/support-us",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/spatie",
+ "type": "github"
+ }
+ ],
+ "time": "2025-12-03T10:29:27+00:00"
+ },
{
"name": "spatie/temporary-directory",
"version": "2.3.1",
@@ -8336,54 +8627,6 @@
],
"time": "2026-01-08T07:23:06+00:00"
},
- {
- "name": "doctrine/deprecations",
- "version": "1.1.5",
- "source": {
- "type": "git",
- "url": "https://github.com/doctrine/deprecations.git",
- "reference": "459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/doctrine/deprecations/zipball/459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38",
- "reference": "459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38",
- "shasum": ""
- },
- "require": {
- "php": "^7.1 || ^8.0"
- },
- "conflict": {
- "phpunit/phpunit": "<=7.5 || >=13"
- },
- "require-dev": {
- "doctrine/coding-standard": "^9 || ^12 || ^13",
- "phpstan/phpstan": "1.4.10 || 2.1.11",
- "phpstan/phpstan-phpunit": "^1.0 || ^2",
- "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6 || ^10.5 || ^11.5 || ^12",
- "psr/log": "^1 || ^2 || ^3"
- },
- "suggest": {
- "psr/log": "Allows logging deprecations via PSR-3 logger implementation"
- },
- "type": "library",
- "autoload": {
- "psr-4": {
- "Doctrine\\Deprecations\\": "src"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "description": "A small layer on top of trigger_error(E_USER_DEPRECATED) or PSR-3 logging with options to disable all deprecations or selectively for packages.",
- "homepage": "https://www.doctrine-project.org/",
- "support": {
- "issues": "https://github.com/doctrine/deprecations/issues",
- "source": "https://github.com/doctrine/deprecations/tree/1.1.5"
- },
- "time": "2025-04-07T20:06:18+00:00"
- },
{
"name": "fakerphp/faker",
"version": "v1.24.1",
@@ -10121,59 +10364,6 @@
},
"time": "2022-02-21T01:04:05+00:00"
},
- {
- "name": "phpdocumentor/reflection-common",
- "version": "2.2.0",
- "source": {
- "type": "git",
- "url": "https://github.com/phpDocumentor/ReflectionCommon.git",
- "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b",
- "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b",
- "shasum": ""
- },
- "require": {
- "php": "^7.2 || ^8.0"
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-2.x": "2.x-dev"
- }
- },
- "autoload": {
- "psr-4": {
- "phpDocumentor\\Reflection\\": "src/"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Jaap van Otterdijk",
- "email": "opensource@ijaap.nl"
- }
- ],
- "description": "Common reflection classes used by phpdocumentor to reflect the code structure",
- "homepage": "http://www.phpdoc.org",
- "keywords": [
- "FQSEN",
- "phpDocumentor",
- "phpdoc",
- "reflection",
- "static analysis"
- ],
- "support": {
- "issues": "https://github.com/phpDocumentor/ReflectionCommon/issues",
- "source": "https://github.com/phpDocumentor/ReflectionCommon/tree/2.x"
- },
- "time": "2020-06-27T09:03:43+00:00"
- },
{
"name": "phpdocumentor/reflection-docblock",
"version": "5.6.6",
@@ -10238,111 +10428,6 @@
},
"time": "2025-12-22T21:13:58+00:00"
},
- {
- "name": "phpdocumentor/type-resolver",
- "version": "1.12.0",
- "source": {
- "type": "git",
- "url": "https://github.com/phpDocumentor/TypeResolver.git",
- "reference": "92a98ada2b93d9b201a613cb5a33584dde25f195"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/92a98ada2b93d9b201a613cb5a33584dde25f195",
- "reference": "92a98ada2b93d9b201a613cb5a33584dde25f195",
- "shasum": ""
- },
- "require": {
- "doctrine/deprecations": "^1.0",
- "php": "^7.3 || ^8.0",
- "phpdocumentor/reflection-common": "^2.0",
- "phpstan/phpdoc-parser": "^1.18|^2.0"
- },
- "require-dev": {
- "ext-tokenizer": "*",
- "phpbench/phpbench": "^1.2",
- "phpstan/extension-installer": "^1.1",
- "phpstan/phpstan": "^1.8",
- "phpstan/phpstan-phpunit": "^1.1",
- "phpunit/phpunit": "^9.5",
- "rector/rector": "^0.13.9",
- "vimeo/psalm": "^4.25"
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-1.x": "1.x-dev"
- }
- },
- "autoload": {
- "psr-4": {
- "phpDocumentor\\Reflection\\": "src"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Mike van Riel",
- "email": "me@mikevanriel.com"
- }
- ],
- "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names",
- "support": {
- "issues": "https://github.com/phpDocumentor/TypeResolver/issues",
- "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.12.0"
- },
- "time": "2025-11-21T15:09:14+00:00"
- },
- {
- "name": "phpstan/phpdoc-parser",
- "version": "2.3.1",
- "source": {
- "type": "git",
- "url": "https://github.com/phpstan/phpdoc-parser.git",
- "reference": "16dbf9937da8d4528ceb2145c9c7c0bd29e26374"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/16dbf9937da8d4528ceb2145c9c7c0bd29e26374",
- "reference": "16dbf9937da8d4528ceb2145c9c7c0bd29e26374",
- "shasum": ""
- },
- "require": {
- "php": "^7.4 || ^8.0"
- },
- "require-dev": {
- "doctrine/annotations": "^2.0",
- "nikic/php-parser": "^5.3.0",
- "php-parallel-lint/php-parallel-lint": "^1.2",
- "phpstan/extension-installer": "^1.0",
- "phpstan/phpstan": "^2.0",
- "phpstan/phpstan-phpunit": "^2.0",
- "phpstan/phpstan-strict-rules": "^2.0",
- "phpunit/phpunit": "^9.6",
- "symfony/process": "^5.2"
- },
- "type": "library",
- "autoload": {
- "psr-4": {
- "PHPStan\\PhpDocParser\\": [
- "src/"
- ]
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "description": "PHPDoc parser with support for nullable, intersection and generic types",
- "support": {
- "issues": "https://github.com/phpstan/phpdoc-parser/issues",
- "source": "https://github.com/phpstan/phpdoc-parser/tree/2.3.1"
- },
- "time": "2026-01-12T11:33:04+00:00"
- },
{
"name": "phpstan/phpstan",
"version": "2.1.33",
diff --git a/config/app.php b/config/app.php
index c7cb051..6a47a72 100644
--- a/config/app.php
+++ b/config/app.php
@@ -153,4 +153,6 @@ return [
'version' => env('APP_VERSION', null),
'catalog_url' => env('CATALOG_URL', 'https://raw.githubusercontent.com/bnussbau/trmnl-recipe-catalog/refs/heads/main/catalog.yaml'),
+
+ 'github_repo' => env('GITHUB_REPO', 'usetrmnl/byos_laravel'),
];
diff --git a/config/settings.php b/config/settings.php
new file mode 100644
index 0000000..1065503
--- /dev/null
+++ b/config/settings.php
@@ -0,0 +1,94 @@
+ [
+
+ ],
+
+ /*
+ * The path where the settings classes will be created.
+ */
+ 'setting_class_path' => app_path('Settings'),
+
+ /*
+ * In these directories settings migrations will be stored and ran when migrating. A settings
+ * migration created via the make:settings-migration command will be stored in the first path or
+ * a custom defined path when running the command.
+ */
+ 'migrations_paths' => [
+ database_path('settings'),
+ ],
+
+ /*
+ * When no repository was set for a settings class the following repository
+ * will be used for loading and saving settings.
+ */
+ 'default_repository' => 'database',
+
+ /*
+ * Settings will be stored and loaded from these repositories.
+ */
+ 'repositories' => [
+ 'database' => [
+ 'type' => Spatie\LaravelSettings\SettingsRepositories\DatabaseSettingsRepository::class,
+ 'model' => null,
+ 'table' => null,
+ 'connection' => null,
+ ],
+ 'redis' => [
+ 'type' => Spatie\LaravelSettings\SettingsRepositories\RedisSettingsRepository::class,
+ 'connection' => null,
+ 'prefix' => null,
+ ],
+ ],
+
+ /*
+ * The encoder and decoder will determine how settings are stored and
+ * retrieved in the database. By default, `json_encode` and `json_decode`
+ * are used.
+ */
+ 'encoder' => null,
+ 'decoder' => null,
+
+ /*
+ * The contents of settings classes can be cached through your application,
+ * settings will be stored within a provided Laravel store and can have an
+ * additional prefix.
+ */
+ 'cache' => [
+ 'enabled' => env('SETTINGS_CACHE_ENABLED', false),
+ 'store' => null,
+ 'prefix' => null,
+ 'ttl' => null,
+ ],
+
+ /*
+ * These global casts will be automatically used whenever a property within
+ * your settings class isn't a default PHP type.
+ */
+ 'global_casts' => [
+ DateTimeInterface::class => Spatie\LaravelSettings\SettingsCasts\DateTimeInterfaceCast::class,
+ DateTimeZone::class => Spatie\LaravelSettings\SettingsCasts\DateTimeZoneCast::class,
+ // Spatie\DataTransferObject\DataTransferObject::class => Spatie\LaravelSettings\SettingsCasts\DtoCast::class,
+ Spatie\LaravelData\Data::class => Spatie\LaravelSettings\SettingsCasts\DataCast::class,
+ ],
+
+ /*
+ * The package will look for settings in these paths and automatically
+ * register them.
+ */
+ 'auto_discover_settings' => [
+ app_path('Settings'),
+ ],
+
+ /*
+ * Automatically discovered settings classes can be cached, so they don't
+ * need to be searched each time the application boots up.
+ */
+ 'discovered_settings_cache_path' => base_path('bootstrap/cache'),
+];
diff --git a/database/migrations/2026_01_16_083707_create_settings_table.php b/database/migrations/2026_01_16_083707_create_settings_table.php
new file mode 100644
index 0000000..9b14b86
--- /dev/null
+++ b/database/migrations/2026_01_16_083707_create_settings_table.php
@@ -0,0 +1,24 @@
+id();
+
+ $table->string('group');
+ $table->string('name');
+ $table->boolean('locked')->default(false);
+ $table->json('payload');
+
+ $table->timestamps();
+
+ $table->unique(['group', 'name']);
+ });
+ }
+};
diff --git a/database/settings/2026_01_16_102043_create_update_settings.php b/database/settings/2026_01_16_102043_create_update_settings.php
new file mode 100644
index 0000000..02be86d
--- /dev/null
+++ b/database/settings/2026_01_16_102043_create_update_settings.php
@@ -0,0 +1,11 @@
+migrator->add('update.prereleases', false);
+ }
+};
diff --git a/resources/views/layouts/app/header.blade.php b/resources/views/layouts/app/header.blade.php
index 7a9f2c0..740e3b1 100644
--- a/resources/views/layouts/app/header.blade.php
+++ b/resources/views/layouts/app/header.blade.php
@@ -70,6 +70,12 @@