10 Commits

Author SHA1 Message Date
80a8b86a08 fixed bootstrap/cache
All checks were successful
CI/CD Pipeline / test (push) Successful in 2s
CI/CD Pipeline / deploy (push) Successful in 21s
CI/CD Pipeline / promote_stable (push) Successful in 2s
2026-02-13 10:15:30 +01:00
c1cb3f394a prepare for cli updates with custom php binary
Some checks failed
CI/CD Pipeline / test (push) Successful in 3s
CI/CD Pipeline / deploy (push) Failing after 12s
CI/CD Pipeline / promote_stable (push) Has been skipped
2026-02-13 10:11:34 +01:00
31c8491aaf Update composer.lock
Some checks failed
CI/CD Pipeline / test (push) Successful in 2s
CI/CD Pipeline / deploy (push) Failing after 10s
CI/CD Pipeline / promote_stable (push) Has been skipped
2026-02-13 10:08:16 +01:00
0ad5916504 Add custom paths and ACP tab
Some checks failed
CI/CD Pipeline / test (push) Successful in 12s
CI/CD Pipeline / deploy (push) Failing after 11s
CI/CD Pipeline / promote_stable (push) Has been skipped
2026-02-13 10:04:28 +01:00
bac70c3927 Empty
Some checks failed
CI/CD Pipeline / test (push) Successful in 3s
CI/CD Pipeline / deploy (push) Failing after 15s
CI/CD Pipeline / promote_stable (push) Has been skipped
2026-02-13 10:03:14 +01:00
bf23e46e2d Polish ACP system layout
Some checks failed
CI/CD Pipeline / test (push) Successful in 2s
CI/CD Pipeline / deploy (push) Failing after 11s
CI/CD Pipeline / promote_stable (push) Has been skipped
2026-02-12 20:02:28 +01:00
55b9a69c42 Refine ACP system settings
Some checks failed
CI/CD Pipeline / test (push) Successful in 4s
CI/CD Pipeline / deploy (push) Failing after 19s
CI/CD Pipeline / promote_stable (push) Has been skipped
2026-02-12 19:44:23 +01:00
b6ce5160f9 prepare for cli updates with custom php binary
Some checks failed
CI/CD Pipeline / test (push) Successful in 3s
CI/CD Pipeline / deploy (push) Failing after 10s
CI/CD Pipeline / promote_stable (push) Has been skipped
2026-02-11 19:46:33 +01:00
d279e7f36f prepare for cli updates with custom php binary
Some checks failed
CI/CD Pipeline / test (push) Successful in 2s
CI/CD Pipeline / deploy (push) Failing after 9s
CI/CD Pipeline / promote_stable (push) Has been skipped
2026-02-11 19:13:59 +01:00
a0d914ea24 Stop tracking bootstrap cache
Some checks failed
CI/CD Pipeline / test (push) Successful in 2s
CI/CD Pipeline / deploy (push) Failing after 10s
CI/CD Pipeline / promote_stable (push) Has been skipped
2026-02-11 19:13:27 +01:00
12 changed files with 328 additions and 648 deletions

2
.gitignore vendored
View File

@@ -22,12 +22,14 @@
/public/build /public/build
/public/hot /public/hot
/public/storage /public/storage
/public/custom
/storage/app /storage/app
/storage/framework /storage/framework
/storage/*.key /storage/*.key
/storage/pail /storage/pail
/storage/framework/views/*.php /storage/framework/views/*.php
/bootstrap/cache/*.php /bootstrap/cache/*.php
/custom
/vendor /vendor
Homestead.json Homestead.json
Homestead.yaml Homestead.yaml

View File

@@ -1,5 +1,12 @@
# Changelog # Changelog
## 2026-02-12
- Refined ACP System tab with left navigation, section-specific requirements, and CLI PHP selector.
- Added CLI PHP interpreter options (php, keyhelp-php-domain, custom) with KeyHelp guidance.
- Updated CLI update tooling and automation notes (KeyHelp PHP handling, CI runner requirements).
- Adjusted ACP layout and tab styling for better dark-mode readability and auto-sizing sidebars.
- Added Custom top-level ACP tab and preserved /custom paths during in-app updates.
## 2026-02-10 ## 2026-02-10
- Reshaped ACP System tab with left navigation and dedicated views (Overview, Live Update, CLI, CI/CD). - Reshaped ACP System tab with left navigation and dedicated views (Overview, Live Update, CLI, CI/CD).
- Moved system requirements table into the CI/CD view with refresh controls. - Moved system requirements table into the CI/CD view with refresh controls.

View File

@@ -9,4 +9,7 @@ Progress (last 2 days):
- Added coverage scripts and cleanup (tests for update/version flows, system update/status, attachments, forums, roles, ranks, settings, portal, etc.). - Added coverage scripts and cleanup (tests for update/version flows, system update/status, attachments, forums, roles, ranks, settings, portal, etc.).
- Hardened tests with fakes/mocks to cover error paths and edge cases. - Hardened tests with fakes/mocks to cover error paths and edge cases.
TODO: Make PHP binary path configurable for updates if default PHP is outdated (ACP -> System). TODO: Make the PHP binary path configurable for updates if the default PHP is outdated (ACP -> System).
CI/CD: Runner must have PHP 8.4+ as the default CLI interpreter.
KeyHelp: `keyhelp-php-domain` can select the PHP version based on the domain of the script location.
KeyHelp: `keyhelp-php-domain` is a Pro feature; on non-Pro setups we must fake the command.

View File

@@ -21,12 +21,31 @@
path: "{{ prod_base_dir }}/.env" path: "{{ prod_base_dir }}/.env"
register: env_file register: env_file
- name: Ensure bootstrap cache directory exists
file:
path: "{{ prod_base_dir }}/bootstrap/cache"
state: directory
mode: "0775"
- name: Download and installs all libs and dependencies - name: Download and installs all libs and dependencies
block:
- name: Composer install
community.general.composer: community.general.composer:
command: install command: install
arguments: --no-dev --optimize-autoloader arguments: --no-dev --optimize-autoloader
working_dir: "{{ prod_base_dir }}" working_dir: "{{ prod_base_dir }}"
php_path: /usr/bin/keyhelp-php84 php_path: /usr/bin/keyhelp-php84
rescue:
- name: Debug package discovery
shell: |
keyhelp-php84 artisan package:discover -v --ansi 2>&1 | tail -n 200
args:
chdir: "{{ prod_base_dir }}"
register: package_discover_debug
- debug:
var: package_discover_debug.stdout_lines
- fail:
msg: "Composer install failed; see package:discover output above."
- name: Install node_modules - name: Install node_modules
npm: npm:

View File

@@ -113,6 +113,7 @@ class SystemUpdateController extends Controller
$append('Syncing files...'); $append('Syncing files...');
$usedRsync = false; $usedRsync = false;
$rsyncPath = trim((string) shell_exec('command -v rsync')); $rsyncPath = trim((string) shell_exec('command -v rsync'));
$protectedPaths = ['custom', 'public/custom'];
if ($rsyncPath !== '') { if ($rsyncPath !== '') {
$usedRsync = true; $usedRsync = true;
$rsync = new Process([ $rsync = new Process([
@@ -122,6 +123,8 @@ class SystemUpdateController extends Controller
'--exclude=.env', '--exclude=.env',
'--exclude=storage', '--exclude=storage',
'--exclude=public/storage', '--exclude=public/storage',
'--exclude=custom',
'--exclude=public/custom',
$sourceDir . '/', $sourceDir . '/',
base_path() . '/', base_path() . '/',
]); ]);
@@ -134,6 +137,15 @@ class SystemUpdateController extends Controller
], 500); ], 500);
} }
} else { } else {
foreach ($protectedPaths as $path) {
$sourcePath = $sourceDir . DIRECTORY_SEPARATOR . $path;
if (File::exists($sourcePath)) {
File::deleteDirectory($sourcePath);
if (File::exists($sourcePath)) {
File::delete($sourcePath);
}
}
}
File::copyDirectory($sourceDir, base_path()); File::copyDirectory($sourceDir, base_path());
} }

View File

@@ -1,72 +0,0 @@
<?php return array (
'barryvdh/laravel-ide-helper' =>
array (
'providers' =>
array (
0 => 'Barryvdh\\LaravelIdeHelper\\IdeHelperServiceProvider',
),
),
'laravel/fortify' =>
array (
'providers' =>
array (
0 => 'Laravel\\Fortify\\FortifyServiceProvider',
),
),
'laravel/pail' =>
array (
'providers' =>
array (
0 => 'Laravel\\Pail\\PailServiceProvider',
),
),
'laravel/sail' =>
array (
'providers' =>
array (
0 => 'Laravel\\Sail\\SailServiceProvider',
),
),
'laravel/sanctum' =>
array (
'providers' =>
array (
0 => 'Laravel\\Sanctum\\SanctumServiceProvider',
),
),
'laravel/tinker' =>
array (
'providers' =>
array (
0 => 'Laravel\\Tinker\\TinkerServiceProvider',
),
),
'nesbot/carbon' =>
array (
'providers' =>
array (
0 => 'Carbon\\Laravel\\ServiceProvider',
),
),
'nunomaduro/collision' =>
array (
'providers' =>
array (
0 => 'NunoMaduro\\Collision\\Adapters\\Laravel\\CollisionServiceProvider',
),
),
'nunomaduro/termwind' =>
array (
'providers' =>
array (
0 => 'Termwind\\Laravel\\TermwindServiceProvider',
),
),
'pestphp/pest-plugin-laravel' =>
array (
'providers' =>
array (
0 => 'Pest\\Laravel\\PestServiceProvider',
),
),
);

View File

@@ -1,277 +0,0 @@
<?php return array (
'providers' =>
array (
0 => 'Illuminate\\Auth\\AuthServiceProvider',
1 => 'Illuminate\\Broadcasting\\BroadcastServiceProvider',
2 => 'Illuminate\\Bus\\BusServiceProvider',
3 => 'Illuminate\\Cache\\CacheServiceProvider',
4 => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
5 => 'Illuminate\\Concurrency\\ConcurrencyServiceProvider',
6 => 'Illuminate\\Cookie\\CookieServiceProvider',
7 => 'Illuminate\\Database\\DatabaseServiceProvider',
8 => 'Illuminate\\Encryption\\EncryptionServiceProvider',
9 => 'Illuminate\\Filesystem\\FilesystemServiceProvider',
10 => 'Illuminate\\Foundation\\Providers\\FoundationServiceProvider',
11 => 'Illuminate\\Hashing\\HashServiceProvider',
12 => 'Illuminate\\Mail\\MailServiceProvider',
13 => 'Illuminate\\Notifications\\NotificationServiceProvider',
14 => 'Illuminate\\Pagination\\PaginationServiceProvider',
15 => 'Illuminate\\Auth\\Passwords\\PasswordResetServiceProvider',
16 => 'Illuminate\\Pipeline\\PipelineServiceProvider',
17 => 'Illuminate\\Queue\\QueueServiceProvider',
18 => 'Illuminate\\Redis\\RedisServiceProvider',
19 => 'Illuminate\\Session\\SessionServiceProvider',
20 => 'Illuminate\\Translation\\TranslationServiceProvider',
21 => 'Illuminate\\Validation\\ValidationServiceProvider',
22 => 'Illuminate\\View\\ViewServiceProvider',
23 => 'Barryvdh\\LaravelIdeHelper\\IdeHelperServiceProvider',
24 => 'Laravel\\Fortify\\FortifyServiceProvider',
25 => 'Laravel\\Pail\\PailServiceProvider',
26 => 'Laravel\\Sail\\SailServiceProvider',
27 => 'Laravel\\Sanctum\\SanctumServiceProvider',
28 => 'Laravel\\Tinker\\TinkerServiceProvider',
29 => 'Carbon\\Laravel\\ServiceProvider',
30 => 'NunoMaduro\\Collision\\Adapters\\Laravel\\CollisionServiceProvider',
31 => 'Termwind\\Laravel\\TermwindServiceProvider',
32 => 'Pest\\Laravel\\PestServiceProvider',
33 => 'App\\Providers\\AppServiceProvider',
34 => 'App\\Providers\\FortifyServiceProvider',
),
'eager' =>
array (
0 => 'Illuminate\\Auth\\AuthServiceProvider',
1 => 'Illuminate\\Cookie\\CookieServiceProvider',
2 => 'Illuminate\\Database\\DatabaseServiceProvider',
3 => 'Illuminate\\Encryption\\EncryptionServiceProvider',
4 => 'Illuminate\\Filesystem\\FilesystemServiceProvider',
5 => 'Illuminate\\Foundation\\Providers\\FoundationServiceProvider',
6 => 'Illuminate\\Notifications\\NotificationServiceProvider',
7 => 'Illuminate\\Pagination\\PaginationServiceProvider',
8 => 'Illuminate\\Session\\SessionServiceProvider',
9 => 'Illuminate\\View\\ViewServiceProvider',
10 => 'Laravel\\Fortify\\FortifyServiceProvider',
11 => 'Laravel\\Pail\\PailServiceProvider',
12 => 'Laravel\\Sanctum\\SanctumServiceProvider',
13 => 'Carbon\\Laravel\\ServiceProvider',
14 => 'NunoMaduro\\Collision\\Adapters\\Laravel\\CollisionServiceProvider',
15 => 'Termwind\\Laravel\\TermwindServiceProvider',
16 => 'Pest\\Laravel\\PestServiceProvider',
17 => 'App\\Providers\\AppServiceProvider',
18 => 'App\\Providers\\FortifyServiceProvider',
),
'deferred' =>
array (
'Illuminate\\Broadcasting\\BroadcastManager' => 'Illuminate\\Broadcasting\\BroadcastServiceProvider',
'Illuminate\\Contracts\\Broadcasting\\Factory' => 'Illuminate\\Broadcasting\\BroadcastServiceProvider',
'Illuminate\\Contracts\\Broadcasting\\Broadcaster' => 'Illuminate\\Broadcasting\\BroadcastServiceProvider',
'Illuminate\\Bus\\Dispatcher' => 'Illuminate\\Bus\\BusServiceProvider',
'Illuminate\\Contracts\\Bus\\Dispatcher' => 'Illuminate\\Bus\\BusServiceProvider',
'Illuminate\\Contracts\\Bus\\QueueingDispatcher' => 'Illuminate\\Bus\\BusServiceProvider',
'Illuminate\\Bus\\BatchRepository' => 'Illuminate\\Bus\\BusServiceProvider',
'Illuminate\\Bus\\DatabaseBatchRepository' => 'Illuminate\\Bus\\BusServiceProvider',
'cache' => 'Illuminate\\Cache\\CacheServiceProvider',
'cache.store' => 'Illuminate\\Cache\\CacheServiceProvider',
'cache.psr6' => 'Illuminate\\Cache\\CacheServiceProvider',
'memcached.connector' => 'Illuminate\\Cache\\CacheServiceProvider',
'Illuminate\\Cache\\RateLimiter' => 'Illuminate\\Cache\\CacheServiceProvider',
'Illuminate\\Foundation\\Console\\AboutCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Cache\\Console\\ClearCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Cache\\Console\\ForgetCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ClearCompiledCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Auth\\Console\\ClearResetsCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ConfigCacheCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ConfigClearCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ConfigShowCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Database\\Console\\DbCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Database\\Console\\MonitorCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Database\\Console\\PruneCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Database\\Console\\ShowCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Database\\Console\\TableCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Database\\Console\\WipeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\DownCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\EnvironmentCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\EnvironmentDecryptCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\EnvironmentEncryptCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\EventCacheCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\EventClearCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\EventListCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Concurrency\\Console\\InvokeSerializedClosureCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\KeyGenerateCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\OptimizeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\OptimizeClearCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\PackageDiscoverCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Cache\\Console\\PruneStaleTagsCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Queue\\Console\\ClearCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Queue\\Console\\ListFailedCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Queue\\Console\\FlushFailedCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Queue\\Console\\ForgetFailedCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Queue\\Console\\ListenCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Queue\\Console\\MonitorCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Queue\\Console\\PauseCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Queue\\Console\\PruneBatchesCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Queue\\Console\\PruneFailedJobsCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Queue\\Console\\RestartCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Queue\\Console\\ResumeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Queue\\Console\\RetryCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Queue\\Console\\RetryBatchCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Queue\\Console\\WorkCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ReloadCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\RouteCacheCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\RouteClearCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\RouteListCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Database\\Console\\DumpCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Database\\Console\\Seeds\\SeedCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Console\\Scheduling\\ScheduleFinishCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Console\\Scheduling\\ScheduleListCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Console\\Scheduling\\ScheduleRunCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Console\\Scheduling\\ScheduleClearCacheCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Console\\Scheduling\\ScheduleTestCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Console\\Scheduling\\ScheduleWorkCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Console\\Scheduling\\ScheduleInterruptCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Database\\Console\\ShowModelCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\StorageLinkCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\StorageUnlinkCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\UpCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ViewCacheCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ViewClearCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ApiInstallCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\BroadcastingInstallCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Cache\\Console\\CacheTableCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\CastMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ChannelListCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ChannelMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ClassMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ComponentMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ConfigMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ConfigPublishCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ConsoleMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Routing\\Console\\ControllerMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\DocsCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\EnumMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\EventGenerateCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\EventMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ExceptionMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Database\\Console\\Factories\\FactoryMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\InterfaceMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\JobMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\JobMiddlewareMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\LangPublishCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ListenerMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\MailMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Routing\\Console\\MiddlewareMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ModelMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\NotificationMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Notifications\\Console\\NotificationTableCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ObserverMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\PolicyMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ProviderMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Queue\\Console\\FailedTableCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Queue\\Console\\TableCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Queue\\Console\\BatchesTableCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\RequestMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ResourceMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\RuleMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ScopeMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Database\\Console\\Seeds\\SeederMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Session\\Console\\SessionTableCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ServeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\StubPublishCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\TestMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\TraitMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\VendorPublishCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ViewMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'migrator' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'migration.repository' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'migration.creator' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Database\\Migrations\\Migrator' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Database\\Console\\Migrations\\MigrateCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Database\\Console\\Migrations\\FreshCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Database\\Console\\Migrations\\InstallCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Database\\Console\\Migrations\\RefreshCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Database\\Console\\Migrations\\ResetCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Database\\Console\\Migrations\\RollbackCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Database\\Console\\Migrations\\StatusCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Database\\Console\\Migrations\\MigrateMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'composer' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Concurrency\\ConcurrencyManager' => 'Illuminate\\Concurrency\\ConcurrencyServiceProvider',
'hash' => 'Illuminate\\Hashing\\HashServiceProvider',
'hash.driver' => 'Illuminate\\Hashing\\HashServiceProvider',
'mail.manager' => 'Illuminate\\Mail\\MailServiceProvider',
'mailer' => 'Illuminate\\Mail\\MailServiceProvider',
'Illuminate\\Mail\\Markdown' => 'Illuminate\\Mail\\MailServiceProvider',
'auth.password' => 'Illuminate\\Auth\\Passwords\\PasswordResetServiceProvider',
'auth.password.broker' => 'Illuminate\\Auth\\Passwords\\PasswordResetServiceProvider',
'Illuminate\\Contracts\\Pipeline\\Hub' => 'Illuminate\\Pipeline\\PipelineServiceProvider',
'pipeline' => 'Illuminate\\Pipeline\\PipelineServiceProvider',
'queue' => 'Illuminate\\Queue\\QueueServiceProvider',
'queue.connection' => 'Illuminate\\Queue\\QueueServiceProvider',
'queue.failer' => 'Illuminate\\Queue\\QueueServiceProvider',
'queue.listener' => 'Illuminate\\Queue\\QueueServiceProvider',
'queue.worker' => 'Illuminate\\Queue\\QueueServiceProvider',
'redis' => 'Illuminate\\Redis\\RedisServiceProvider',
'redis.connection' => 'Illuminate\\Redis\\RedisServiceProvider',
'translator' => 'Illuminate\\Translation\\TranslationServiceProvider',
'translation.loader' => 'Illuminate\\Translation\\TranslationServiceProvider',
'validator' => 'Illuminate\\Validation\\ValidationServiceProvider',
'validation.presence' => 'Illuminate\\Validation\\ValidationServiceProvider',
'Illuminate\\Contracts\\Validation\\UncompromisedVerifier' => 'Illuminate\\Validation\\ValidationServiceProvider',
'Barryvdh\\LaravelIdeHelper\\Console\\GeneratorCommand' => 'Barryvdh\\LaravelIdeHelper\\IdeHelperServiceProvider',
'Barryvdh\\LaravelIdeHelper\\Console\\ModelsCommand' => 'Barryvdh\\LaravelIdeHelper\\IdeHelperServiceProvider',
'Barryvdh\\LaravelIdeHelper\\Console\\MetaCommand' => 'Barryvdh\\LaravelIdeHelper\\IdeHelperServiceProvider',
'Barryvdh\\LaravelIdeHelper\\Console\\EloquentCommand' => 'Barryvdh\\LaravelIdeHelper\\IdeHelperServiceProvider',
'Laravel\\Sail\\Console\\InstallCommand' => 'Laravel\\Sail\\SailServiceProvider',
'Laravel\\Sail\\Console\\PublishCommand' => 'Laravel\\Sail\\SailServiceProvider',
'command.tinker' => 'Laravel\\Tinker\\TinkerServiceProvider',
),
'when' =>
array (
'Illuminate\\Broadcasting\\BroadcastServiceProvider' =>
array (
),
'Illuminate\\Bus\\BusServiceProvider' =>
array (
),
'Illuminate\\Cache\\CacheServiceProvider' =>
array (
),
'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider' =>
array (
),
'Illuminate\\Concurrency\\ConcurrencyServiceProvider' =>
array (
),
'Illuminate\\Hashing\\HashServiceProvider' =>
array (
),
'Illuminate\\Mail\\MailServiceProvider' =>
array (
),
'Illuminate\\Auth\\Passwords\\PasswordResetServiceProvider' =>
array (
),
'Illuminate\\Pipeline\\PipelineServiceProvider' =>
array (
),
'Illuminate\\Queue\\QueueServiceProvider' =>
array (
),
'Illuminate\\Redis\\RedisServiceProvider' =>
array (
),
'Illuminate\\Translation\\TranslationServiceProvider' =>
array (
),
'Illuminate\\Validation\\ValidationServiceProvider' =>
array (
),
'Barryvdh\\LaravelIdeHelper\\IdeHelperServiceProvider' =>
array (
),
'Laravel\\Sail\\SailServiceProvider' =>
array (
),
'Laravel\\Tinker\\TinkerServiceProvider' =>
array (
),
),
);

View File

@@ -98,5 +98,5 @@
"minimum-stability": "stable", "minimum-stability": "stable",
"prefer-stable": true, "prefer-stable": true,
"version": "26.0.2", "version": "26.0.2",
"build": "56" "build": "66"
} }

2
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "83b577a02e99a4e17696941851d13cc2", "content-hash": "e6076a6989b155fddbc675cab28fdd50",
"packages": [ "packages": [
{ {
"name": "bacon/bacon-qr-code", "name": "bacon/bacon-qr-code",

View File

@@ -4,10 +4,17 @@ set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$ROOT_DIR" cd "$ROOT_DIR"
if [[ -n "$(git status --porcelain)" ]]; then git restore -q bootstrap/cache/packages.php bootstrap/cache/services.php 2>/dev/null || true
DIRTY="$(git status --porcelain)"
DIRTY_FILTERED="$(echo "$DIRTY" | grep -vE '^( M|M ) (bootstrap/cache/(packages|services)\.php|package-lock\.json)$' || true)"
if [[ -n "$DIRTY_FILTERED" ]]; then
echo "Working tree is dirty. Please commit or stash changes before updating." echo "Working tree is dirty. Please commit or stash changes before updating."
echo "$DIRTY_FILTERED"
exit 1 exit 1
fi fi
if echo "$DIRTY" | grep -qE 'package-lock\.json'; then
echo "Warning: package-lock.json is modified. Continuing anyway."
fi
echo "Fetching latest refs..." echo "Fetching latest refs..."
git fetch --prune --tags git fetch --prune --tags

View File

@@ -862,8 +862,8 @@ a {
} }
[data-bs-theme="dark"] { [data-bs-theme="dark"] {
--bb-ink: #e6e8eb; --bb-ink: #aaaeb4;
--bb-ink-muted: #9aa4b2; --bb-ink-muted: #6b7483;
--bb-border: #2a2f3a; --bb-border: #2a2f3a;
--bb-page-bg: radial-gradient(circle at 10% 20%, #141823 0%, #10131a 45%, #0b0e14 100%); --bb-page-bg: radial-gradient(circle at 10% 20%, #141823 0%, #10131a 45%, #0b0e14 100%);
} }
@@ -922,10 +922,18 @@ a {
.nav-tabs .nav-link { .nav-tabs .nav-link {
color: var(--bb-accent, #f29b3f); color: var(--bb-accent, #f29b3f);
border: 1px solid var(--bb-border);
border-bottom-color: transparent;
border-radius: 10px 10px 0 0;
margin-right: 0.35rem;
background: transparent;
} }
.nav-tabs .nav-link.active { .nav-tabs .nav-link.active {
color: inherit; color: inherit;
background: rgba(255, 255, 255, 0.04);
border-color: var(--bb-border);
border-bottom-color: transparent;
} }
.bb-version { .bb-version {

View File

@@ -202,7 +202,8 @@ function Acp({ isAdmin }) {
favicon_256: '', favicon_256: '',
}) })
const [systemCliSettings, setSystemCliSettings] = useState({ const [systemCliSettings, setSystemCliSettings] = useState({
php_binary: '', php_mode: 'php',
php_custom: '',
}) })
const [systemCliSaving, setSystemCliSaving] = useState(false) const [systemCliSaving, setSystemCliSaving] = useState(false)
const [systemCliError, setSystemCliError] = useState('') const [systemCliError, setSystemCliError] = useState('')
@@ -297,8 +298,15 @@ function Acp({ isAdmin }) {
favicon_256: settingsMap.get('favicon_256') || '', favicon_256: settingsMap.get('favicon_256') || '',
} }
setGeneralSettings(next) setGeneralSettings(next)
const configuredPhp = settingsMap.get('system.php_binary') || ''
const phpMode = configuredPhp === 'keyhelp-php-domain'
? 'keyhelp'
: configuredPhp === '' || configuredPhp === 'php'
? 'php'
: 'custom'
setSystemCliSettings({ setSystemCliSettings({
php_binary: settingsMap.get('system.php_binary') || '', php_mode: phpMode,
php_custom: phpMode === 'custom' ? configuredPhp : '',
}) })
setAttachmentSettings({ setAttachmentSettings({
display_images_inline: settingsMap.get('attachments.display_images_inline') || 'true', display_images_inline: settingsMap.get('attachments.display_images_inline') || 'true',
@@ -386,11 +394,22 @@ function Acp({ isAdmin }) {
setSystemCliSaving(true) setSystemCliSaving(true)
setSystemCliError('') setSystemCliError('')
try { try {
const value = typeof systemCliSettings.php_binary === 'string' let value = ''
? systemCliSettings.php_binary.trim() if (systemCliSettings.php_mode === 'custom') {
: String(systemCliSettings.php_binary ?? '') value = typeof systemCliSettings.php_custom === 'string'
? systemCliSettings.php_custom.trim()
: String(systemCliSettings.php_custom ?? '')
} else if (systemCliSettings.php_mode === 'keyhelp') {
value = 'keyhelp-php-domain'
} else {
value = 'php'
}
await saveSetting('system.php_binary', value) await saveSetting('system.php_binary', value)
setSystemCliSettings((prev) => ({ ...prev, php_binary: value })) setSystemCliSettings((prev) => ({
...prev,
php_mode: systemCliSettings.php_mode,
php_custom: systemCliSettings.php_mode === 'custom' ? value : '',
}))
} catch (err) { } catch (err) {
setSystemCliError(err.message) setSystemCliError(err.message)
} finally { } finally {
@@ -398,6 +417,107 @@ function Acp({ isAdmin }) {
} }
} }
const systemChecks = useMemo(() => {
if (!systemStatus) return []
return [
{
id: 'php',
label: 'PHP',
path: systemStatus.php_selected_path || '—',
min: systemStatus.min_versions?.php || '—',
current: systemStatus.php_selected_version || '—',
status: systemStatus.php_selected_ok ? 'ok' : 'bad',
},
{
id: 'composer',
label: 'Composer',
path: systemStatus.composer || t('system.not_found'),
min: systemStatus.min_versions?.composer || '—',
current: systemStatus.composer_version || '—',
status: systemStatus.composer ? 'ok' : 'bad',
},
{
id: 'node',
label: 'Node',
path: systemStatus.node || t('system.not_found'),
min: systemStatus.min_versions?.node || '—',
current: systemStatus.node_version || '—',
status: systemStatus.node ? 'ok' : 'bad',
},
{
id: 'npm',
label: 'npm',
path: systemStatus.npm || t('system.not_found'),
min: systemStatus.min_versions?.npm || '—',
current: systemStatus.npm_version || '—',
status: systemStatus.npm ? 'ok' : 'bad',
},
{
id: 'tar',
label: 'tar',
path: systemStatus.tar || t('system.not_found'),
min: '—',
current: systemStatus.tar_version || '—',
status: systemStatus.tar ? 'ok' : 'bad',
},
{
id: 'rsync',
label: 'rsync',
path: systemStatus.rsync || t('system.not_found'),
min: '—',
current: systemStatus.rsync_version || '—',
status: systemStatus.rsync ? 'ok' : 'bad',
},
{
id: 'proc',
label: 'proc_* functions',
path: systemStatus.proc_functions
? Object.entries(systemStatus.proc_functions)
.filter(([, ok]) => !ok)
.map(([name]) => name)
.join(', ')
: '—',
min: '—',
current: '—',
note: 'Optional. Needed for automated version checks.',
status:
Boolean(systemStatus.proc_functions) &&
Object.values(systemStatus.proc_functions).every(Boolean)
? 'ok'
: 'bad',
pathColSpan: 3,
},
{
id: 'storage',
label: t('system.storage_writable'),
path: 'storage/',
min: '—',
current: '—',
status: systemStatus.storage_writable ? 'ok' : 'bad',
},
{
id: 'updates',
label: t('system.updates_writable'),
path: 'storage/app/updates',
min: '—',
current: '—',
status: systemStatus.updates_writable ? 'ok' : 'bad',
},
]
}, [systemStatus, t])
const visibleSystemChecks = useMemo(() => {
const visibilityBySection = {
insite: ['php', 'proc', 'storage', 'updates'],
cli: ['php', 'composer', 'node', 'npm', 'proc', 'storage', 'updates'],
ci: ['php', 'composer', 'node', 'npm', 'tar', 'rsync', 'proc', 'storage', 'updates'],
info: [],
}
const allowed = new Set(visibilityBySection[systemSection] || [])
return systemChecks.filter((check) => allowed.has(check.id))
}, [systemChecks, systemSection])
const handleLogoUpload = async (file, settingKey) => { const handleLogoUpload = async (file, settingKey) => {
if (!file) return if (!file) return
setGeneralUploading(true) setGeneralUploading(true)
@@ -848,6 +968,74 @@ function Acp({ isAdmin }) {
} }
} }
function renderSystemRequirementsPanel() {
return (
<div className="bb-acp-panel">
<div className="bb-acp-panel-header">
<div className="d-flex align-items-center justify-content-between">
<h5 className="mb-0">{t('system.requirements')}</h5>
<Button
type="button"
size="sm"
variant="dark"
onClick={loadSystemStatus}
disabled={systemLoading}
>
{t('acp.refresh')}
</Button>
</div>
</div>
<div className="bb-acp-panel-body">
{!systemStatus && <p className="bb-muted mb-0">{t('system.not_found')}</p>}
{systemStatus && (
<table className="bb-acp-stats-table">
<thead>
<tr>
<th>{t('system.check')}</th>
<th>{t('system.path')}</th>
<th>{t('system.min_version')}</th>
<th>{t('system.current_version')}</th>
<th>{t('system.status')}</th>
<th>{t('system.recheck')}</th>
</tr>
</thead>
<tbody>
{visibleSystemChecks.map((check) => (
<tr key={check.id}>
<td>{check.label}</td>
<td className="bb-acp-stats-value text-start" colSpan={check.pathColSpan || 1}>
{check.path}
{check.note && <div className="bb-muted mt-1 text-center">{check.note}</div>}
</td>
{!check.pathColSpan && (
<>
<td className="bb-acp-stats-value">{check.min}</td>
<td className="bb-acp-stats-value">{check.current}</td>
</>
)}
<td className="bb-acp-stats-value">
<StatusIcon status={check.status} />
</td>
<td className="bb-acp-stats-value">
<Button
size="sm"
variant="dark"
onClick={loadSystemStatus}
disabled={systemLoading}
>
{t('system.recheck')}
</Button>
</td>
</tr>
))}
</tbody>
</table>
)}
</div>
</div>
)
}
useEffect(() => { useEffect(() => {
if (isAdmin) { if (isAdmin) {
handleVersionCheck() handleVersionCheck()
@@ -2542,7 +2730,7 @@ function Acp({ isAdmin }) {
<Tabs defaultActiveKey="general" className="mb-3"> <Tabs defaultActiveKey="general" className="mb-3">
<Tab eventKey="general" title={t('acp.general')}> <Tab eventKey="general" title={t('acp.general')}>
<Row className="g-4"> <Row className="g-4">
<Col lg={3} xl={2}> <Col xs={12} lg="auto">
<div className="bb-acp-sidebar"> <div className="bb-acp-sidebar">
<div className="bb-acp-sidebar-section"> <div className="bb-acp-sidebar-section">
<div className="bb-acp-sidebar-title">{t('acp.quick_access')}</div> <div className="bb-acp-sidebar-title">{t('acp.quick_access')}</div>
@@ -2602,13 +2790,7 @@ function Acp({ isAdmin }) {
</div> </div>
</div> </div>
</Col> </Col>
<Col lg={9} xl={10}> <Col xs={12} lg>
<div className="bb-acp-panel mb-4">
<div className="bb-acp-panel-header">
<h5 className="mb-1">{t('acp.welcome_title')}</h5>
<p className="bb-muted mb-0">{t('acp.general_hint')}</p>
</div>
</div>
<div className="bb-acp-panel mb-4"> <div className="bb-acp-panel mb-4">
<div className="bb-acp-panel-header"> <div className="bb-acp-panel-header">
<div className="d-flex align-items-center justify-content-between"> <div className="d-flex align-items-center justify-content-between">
@@ -3535,7 +3717,7 @@ function Acp({ isAdmin }) {
{systemLoading && <p className="bb-muted">{t('acp.loading')}</p>} {systemLoading && <p className="bb-muted">{t('acp.loading')}</p>}
{!systemLoading && ( {!systemLoading && (
<Row className="g-4"> <Row className="g-4">
<Col lg={3} xl={2}> <Col xs={12} lg="auto">
<div className="bb-acp-sidebar"> <div className="bb-acp-sidebar">
<div className="bb-acp-sidebar-section"> <div className="bb-acp-sidebar-section">
<div className="bb-acp-sidebar-title">{t('acp.system')}</div> <div className="bb-acp-sidebar-title">{t('acp.system')}</div>
@@ -3580,7 +3762,7 @@ function Acp({ isAdmin }) {
</div> </div>
</div> </div>
</Col> </Col>
<Col lg={9} xl={10}> <Col xs={12} lg>
{systemSection === 'info' && ( {systemSection === 'info' && (
<div className="bb-acp-panel"> <div className="bb-acp-panel">
<div className="bb-acp-panel-header"> <div className="bb-acp-panel-header">
@@ -3595,17 +3777,14 @@ function Acp({ isAdmin }) {
</div> </div>
)} )}
{systemSection === 'insite' && ( {systemSection === 'insite' && (
<div className="bb-acp-panel"> <>
<div className="bb-acp-panel-header"> <div className="bb-acp-panel mb-3">
<h5 className="mb-0">Live Update</h5>
</div>
<div className="bb-acp-panel-body"> <div className="bb-acp-panel-body">
<p className="bb-muted mb-0"> <p className="bb-muted mb-0">Live update controls will appear here.</p>
Placeholder: run a live update from inside the forum, with safety checks
and status details.
</p>
</div> </div>
</div> </div>
{renderSystemRequirementsPanel()}
</>
)} )}
{systemSection === 'cli' && ( {systemSection === 'cli' && (
<div className="bb-acp-panel"> <div className="bb-acp-panel">
@@ -3617,20 +3796,36 @@ function Acp({ isAdmin }) {
<Form onSubmit={handleSystemCliSave}> <Form onSubmit={handleSystemCliSave}>
<Form.Group className="mb-3"> <Form.Group className="mb-3">
<Form.Label>PHP interpreter</Form.Label> <Form.Label>PHP interpreter</Form.Label>
<Form.Control <Form.Select
type="text" className="mb-2"
placeholder={systemStatus?.php_default || '/usr/bin/php'} value={systemCliSettings.php_mode}
value={systemCliSettings.php_binary}
onChange={(event) => onChange={(event) =>
setSystemCliSettings((prev) => ({ setSystemCliSettings((prev) => ({
...prev, ...prev,
php_binary: event.target.value, php_mode: event.target.value,
}))
}
>
<option value="php">php (system default)</option>
<option value="keyhelp">keyhelp-php-domain</option>
<option value="custom">Custom binary (e.g. keyhelp-php84)</option>
</Form.Select>
{systemCliSettings.php_mode === 'custom' && (
<Form.Control
type="text"
placeholder="keyhelp-php84"
value={systemCliSettings.php_custom}
onChange={(event) =>
setSystemCliSettings((prev) => ({
...prev,
php_custom: event.target.value,
})) }))
} }
/> />
)}
<Form.Text className="bb-muted"> <Form.Text className="bb-muted">
Used for CLI-based updates and maintenance tasks. Leave empty to use Used for CLI-based updates and maintenance tasks. `keyhelp-php-domain`
the system default. is available on KeyHelp Pro; on non-Pro setups use a custom binary.
</Form.Text> </Form.Text>
</Form.Group> </Form.Group>
<Button type="submit" variant="dark" disabled={systemCliSaving}> <Button type="submit" variant="dark" disabled={systemCliSaving}>
@@ -3640,268 +3835,44 @@ function Acp({ isAdmin }) {
</div> </div>
</div> </div>
)} )}
{systemSection === 'cli' && renderSystemRequirementsPanel()}
{systemSection === 'ci' && ( {systemSection === 'ci' && (
<>
<div className="bb-acp-panel"> <div className="bb-acp-panel">
<div className="bb-acp-panel-header"> <div className="bb-acp-panel-header">
<div className="d-flex align-items-center justify-content-between"> <h5 className="mb-0">CI/CD</h5>
<h5 className="mb-0">{t('system.requirements')}</h5>
<Button
type="button"
size="sm"
variant="dark"
onClick={loadSystemStatus}
disabled={systemLoading}
>
{t('acp.refresh')}
</Button>
</div>
</div> </div>
<div className="bb-acp-panel-body"> <div className="bb-acp-panel-body">
{!systemStatus && (
<p className="bb-muted mb-0"> <p className="bb-muted mb-0">
{t('system.not_found')} Placeholder: CI/CD pipelines, runner requirements, and deployment logs will
live here.
</p> </p>
)}
{systemStatus && (
<table className="bb-acp-stats-table">
<thead>
<tr>
<th>{t('system.check')}</th>
<th>{t('system.path')}</th>
<th>{t('system.min_version')}</th>
<th>{t('system.current_version')}</th>
<th>{t('system.status')}</th>
<th>{t('system.recheck')}</th>
</tr>
</thead>
<tbody>
<tr>
<td>PHP</td>
<td className="bb-acp-stats-value">
{systemStatus.php_selected_path || '—'}
</td>
<td className="bb-acp-stats-value">
{systemStatus.min_versions?.php || '—'}
</td>
<td className="bb-acp-stats-value">
{systemStatus.php_selected_version || '—'}
</td>
<td className="bb-acp-stats-value">
<StatusIcon
status={systemStatus.php_selected_ok ? 'ok' : 'bad'}
/>
</td>
<td className="bb-acp-stats-value">
<Button
size="sm"
variant="dark"
onClick={loadSystemStatus}
disabled={systemLoading}
>
{t('system.recheck')}
</Button>
</td>
</tr>
<tr>
<td>Composer</td>
<td className="bb-acp-stats-value">
{systemStatus.composer || t('system.not_found')}
</td>
<td className="bb-acp-stats-value">
{systemStatus.min_versions?.composer || '—'}
</td>
<td className="bb-acp-stats-value">
{systemStatus.composer_version || '—'}
</td>
<td className="bb-acp-stats-value">
<StatusIcon status={systemStatus.composer ? 'ok' : 'bad'} />
</td>
<td className="bb-acp-stats-value">
<Button
size="sm"
variant="dark"
onClick={loadSystemStatus}
disabled={systemLoading}
>
{t('system.recheck')}
</Button>
</td>
</tr>
<tr>
<td>Node</td>
<td className="bb-acp-stats-value">
{systemStatus.node || t('system.not_found')}
</td>
<td className="bb-acp-stats-value">
{systemStatus.min_versions?.node || '—'}
</td>
<td className="bb-acp-stats-value">
{systemStatus.node_version || '—'}
</td>
<td className="bb-acp-stats-value">
<StatusIcon status={systemStatus.node ? 'ok' : 'bad'} />
</td>
<td className="bb-acp-stats-value">
<Button
size="sm"
variant="dark"
onClick={loadSystemStatus}
disabled={systemLoading}
>
{t('system.recheck')}
</Button>
</td>
</tr>
<tr>
<td>npm</td>
<td className="bb-acp-stats-value">
{systemStatus.npm || t('system.not_found')}
</td>
<td className="bb-acp-stats-value">
{systemStatus.min_versions?.npm || '—'}
</td>
<td className="bb-acp-stats-value">
{systemStatus.npm_version || '—'}
</td>
<td className="bb-acp-stats-value">
<StatusIcon status={systemStatus.npm ? 'ok' : 'bad'} />
</td>
<td className="bb-acp-stats-value">
<Button
size="sm"
variant="dark"
onClick={loadSystemStatus}
disabled={systemLoading}
>
{t('system.recheck')}
</Button>
</td>
</tr>
<tr>
<td>tar</td>
<td className="bb-acp-stats-value">
{systemStatus.tar || t('system.not_found')}
</td>
<td className="bb-acp-stats-value"></td>
<td className="bb-acp-stats-value">
{systemStatus.tar_version || '—'}
</td>
<td className="bb-acp-stats-value">
<StatusIcon status={systemStatus.tar ? 'ok' : 'bad'} />
</td>
<td className="bb-acp-stats-value">
<Button
size="sm"
variant="dark"
onClick={loadSystemStatus}
disabled={systemLoading}
>
{t('system.recheck')}
</Button>
</td>
</tr>
<tr>
<td>rsync</td>
<td className="bb-acp-stats-value">
{systemStatus.rsync || t('system.not_found')}
</td>
<td className="bb-acp-stats-value"></td>
<td className="bb-acp-stats-value">
{systemStatus.rsync_version || '—'}
</td>
<td className="bb-acp-stats-value">
<StatusIcon status={systemStatus.rsync ? 'ok' : 'bad'} />
</td>
<td className="bb-acp-stats-value">
<Button
size="sm"
variant="dark"
onClick={loadSystemStatus}
disabled={systemLoading}
>
{t('system.recheck')}
</Button>
</td>
</tr>
<tr>
<td>proc_* functions</td>
<td className="bb-acp-stats-value" colSpan={3}>
{systemStatus.proc_functions
? Object.entries(systemStatus.proc_functions)
.filter(([, ok]) => !ok)
.map(([name]) => name)
.join(', ')
: '—'}
</td>
<td className="bb-acp-stats-value">
<StatusIcon
status={
Boolean(systemStatus.proc_functions) &&
Object.values(systemStatus.proc_functions).every(Boolean)
? 'ok'
: 'bad'
}
/>
</td>
<td className="bb-acp-stats-value">
<Button
size="sm"
variant="dark"
onClick={loadSystemStatus}
disabled={systemLoading}
>
{t('system.recheck')}
</Button>
</td>
</tr>
<tr>
<td>{t('system.storage_writable')}</td>
<td className="bb-acp-stats-value">storage/</td>
<td className="bb-acp-stats-value"></td>
<td className="bb-acp-stats-value"></td>
<td className="bb-acp-stats-value">
<StatusIcon status={systemStatus.storage_writable ? 'ok' : 'bad'} />
</td>
<td className="bb-acp-stats-value">
<Button
size="sm"
variant="dark"
onClick={loadSystemStatus}
disabled={systemLoading}
>
{t('system.recheck')}
</Button>
</td>
</tr>
<tr>
<td>{t('system.updates_writable')}</td>
<td className="bb-acp-stats-value">storage/app/updates</td>
<td className="bb-acp-stats-value"></td>
<td className="bb-acp-stats-value"></td>
<td className="bb-acp-stats-value">
<StatusIcon status={systemStatus.updates_writable ? 'ok' : 'bad'} />
</td>
<td className="bb-acp-stats-value">
<Button
size="sm"
variant="dark"
onClick={loadSystemStatus}
disabled={systemLoading}
>
{t('system.recheck')}
</Button>
</td>
</tr>
</tbody>
</table>
)}
</div> </div>
</div> </div>
{renderSystemRequirementsPanel()}
</>
)} )}
</Col> </Col>
</Row> </Row>
)} )}
</Tab> </Tab>
<Tab eventKey="custom" title="Custom">
<Row className="g-4">
<Col xs={12}>
<div className="bb-acp-panel">
<div className="bb-acp-panel-header">
<h5 className="mb-0">Custom</h5>
</div>
<div className="bb-acp-panel-body">
<p className="bb-muted mb-0">
Place site-specific assets or overrides in `/custom` and `public/custom`.
These paths are preserved during in-place updates.
</p>
</div>
</div>
</Col>
</Row>
</Tab>
</Tabs> </Tabs>
<Modal show={showModal} onHide={handleReset} centered size="lg"> <Modal show={showModal} onHide={handleReset} centered size="lg">
<Modal.Header closeButton closeVariant="white"> <Modal.Header closeButton closeVariant="white">