diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ec16a6..dc3dc38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # 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). + ## 2026-02-10 - 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. diff --git a/NOTES.md b/NOTES.md index fbd5227..9e6ad2b 100644 --- a/NOTES.md +++ b/NOTES.md @@ -9,5 +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.). - 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. diff --git a/composer.json b/composer.json index 01bac62..c2f1e81 100644 --- a/composer.json +++ b/composer.json @@ -98,5 +98,5 @@ "minimum-stability": "stable", "prefer-stable": true, "version": "26.0.2", - "build": "59" + "build": "60" } diff --git a/resources/js/pages/Acp.jsx b/resources/js/pages/Acp.jsx index 6ef03a7..655c2b5 100644 --- a/resources/js/pages/Acp.jsx +++ b/resources/js/pages/Acp.jsx @@ -202,7 +202,8 @@ function Acp({ isAdmin }) { favicon_256: '', }) const [systemCliSettings, setSystemCliSettings] = useState({ - php_binary: '', + php_mode: 'php', + php_custom: '', }) const [systemCliSaving, setSystemCliSaving] = useState(false) const [systemCliError, setSystemCliError] = useState('') @@ -297,8 +298,15 @@ function Acp({ isAdmin }) { favicon_256: settingsMap.get('favicon_256') || '', } setGeneralSettings(next) + const configuredPhp = settingsMap.get('system.php_binary') || '' + const phpMode = configuredPhp === 'keyhelp-php-domain' + ? 'keyhelp' + : configuredPhp === '' || configuredPhp === 'php' + ? 'php' + : 'custom' setSystemCliSettings({ - php_binary: settingsMap.get('system.php_binary') || '', + php_mode: phpMode, + php_custom: phpMode === 'custom' ? configuredPhp : '', }) setAttachmentSettings({ display_images_inline: settingsMap.get('attachments.display_images_inline') || 'true', @@ -386,11 +394,22 @@ function Acp({ isAdmin }) { setSystemCliSaving(true) setSystemCliError('') try { - const value = typeof systemCliSettings.php_binary === 'string' - ? systemCliSettings.php_binary.trim() - : String(systemCliSettings.php_binary ?? '') + let value = '' + if (systemCliSettings.php_mode === 'custom') { + 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) - setSystemCliSettings((prev) => ({ ...prev, php_binary: value })) + setSystemCliSettings((prev) => ({ + ...prev, + php_mode: systemCliSettings.php_mode, + php_custom: systemCliSettings.php_mode === 'custom' ? value : '', + })) } catch (err) { setSystemCliError(err.message) } 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) => { if (!file) return setGeneralUploading(true) @@ -848,6 +968,74 @@ function Acp({ isAdmin }) { } } + function renderSystemRequirementsPanel() { + return ( +
+
+
+
{t('system.requirements')}
+ +
+
+
+ {!systemStatus &&

{t('system.not_found')}

} + {systemStatus && ( + + + + + + + + + + + + + {visibleSystemChecks.map((check) => ( + + + + {!check.pathColSpan && ( + <> + + + + )} + + + + ))} + +
{t('system.check')}{t('system.path')}{t('system.min_version')}{t('system.current_version')}{t('system.status')}{t('system.recheck')}
{check.label} + {check.path} + {check.note &&
{check.note}
} +
{check.min}{check.current} + + + +
+ )} +
+
+ ) + } + useEffect(() => { if (isAdmin) { handleVersionCheck() @@ -3595,17 +3783,20 @@ function Acp({ isAdmin }) { )} {systemSection === 'insite' && ( -
-
-
Live Update
+ <> +
+
+
Live Update
+
+
+

+ Placeholder: run a live update from inside the forum, with safety checks + and status details. +

+
-
-

- Placeholder: run a live update from inside the forum, with safety checks - and status details. -

-
-
+ {renderSystemRequirementsPanel()} + )} {systemSection === 'cli' && (
@@ -3617,20 +3808,36 @@ function Acp({ isAdmin }) {
PHP interpreter - setSystemCliSettings((prev) => ({ ...prev, - php_binary: event.target.value, + php_mode: event.target.value, })) } - /> + > + + + + + {systemCliSettings.php_mode === 'custom' && ( + + setSystemCliSettings((prev) => ({ + ...prev, + php_custom: event.target.value, + })) + } + /> + )} - Used for CLI-based updates and maintenance tasks. Leave empty to use - the system default. + Used for CLI-based updates and maintenance tasks. `keyhelp-php-domain` + is available on KeyHelp Pro; on non-Pro setups use a custom binary.
)} + {systemSection === 'cli' && renderSystemRequirementsPanel()} {systemSection === 'ci' && ( -
-
-
-
{t('system.requirements')}
- + <> +
+
+
CI/CD
+
+
+

+ Placeholder: CI/CD pipelines, runner requirements, and deployment logs will + live here. +

-
- {!systemStatus && ( -

- {t('system.not_found')} -

- )} - {systemStatus && ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
{t('system.check')}{t('system.path')}{t('system.min_version')}{t('system.current_version')}{t('system.status')}{t('system.recheck')}
PHP - {systemStatus.php_selected_path || '—'} - - {systemStatus.min_versions?.php || '—'} - - {systemStatus.php_selected_version || '—'} - - - - -
Composer - {systemStatus.composer || t('system.not_found')} - - {systemStatus.min_versions?.composer || '—'} - - {systemStatus.composer_version || '—'} - - - - -
Node - {systemStatus.node || t('system.not_found')} - - {systemStatus.min_versions?.node || '—'} - - {systemStatus.node_version || '—'} - - - - -
npm - {systemStatus.npm || t('system.not_found')} - - {systemStatus.min_versions?.npm || '—'} - - {systemStatus.npm_version || '—'} - - - - -
tar - {systemStatus.tar || t('system.not_found')} - - {systemStatus.tar_version || '—'} - - - - -
rsync - {systemStatus.rsync || t('system.not_found')} - - {systemStatus.rsync_version || '—'} - - - - -
proc_* functions - {systemStatus.proc_functions - ? Object.entries(systemStatus.proc_functions) - .filter(([, ok]) => !ok) - .map(([name]) => name) - .join(', ') - : '—'} - - - - -
{t('system.storage_writable')}storage/ - - - -
{t('system.updates_writable')}storage/app/updates - - - -
- )} -
-
+ {renderSystemRequirementsPanel()} + )}