Refine ACP system settings
This commit is contained in:
@@ -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.
|
||||
|
||||
4
NOTES.md
4
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.
|
||||
|
||||
@@ -98,5 +98,5 @@
|
||||
"minimum-stability": "stable",
|
||||
"prefer-stable": true,
|
||||
"version": "26.0.2",
|
||||
"build": "59"
|
||||
"build": "60"
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<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(() => {
|
||||
if (isAdmin) {
|
||||
handleVersionCheck()
|
||||
@@ -3595,6 +3783,7 @@ function Acp({ isAdmin }) {
|
||||
</div>
|
||||
)}
|
||||
{systemSection === 'insite' && (
|
||||
<>
|
||||
<div className="bb-acp-panel">
|
||||
<div className="bb-acp-panel-header">
|
||||
<h5 className="mb-0">Live Update</h5>
|
||||
@@ -3606,6 +3795,8 @@ function Acp({ isAdmin }) {
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{renderSystemRequirementsPanel()}
|
||||
</>
|
||||
)}
|
||||
{systemSection === 'cli' && (
|
||||
<div className="bb-acp-panel">
|
||||
@@ -3617,20 +3808,36 @@ function Acp({ isAdmin }) {
|
||||
<Form onSubmit={handleSystemCliSave}>
|
||||
<Form.Group className="mb-3">
|
||||
<Form.Label>PHP interpreter</Form.Label>
|
||||
<Form.Control
|
||||
type="text"
|
||||
placeholder={systemStatus?.php_default || '/usr/bin/php'}
|
||||
value={systemCliSettings.php_binary}
|
||||
<Form.Select
|
||||
className="mb-2"
|
||||
value={systemCliSettings.php_mode}
|
||||
onChange={(event) =>
|
||||
setSystemCliSettings((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">
|
||||
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.
|
||||
</Form.Text>
|
||||
</Form.Group>
|
||||
<Button type="submit" variant="dark" disabled={systemCliSaving}>
|
||||
@@ -3640,263 +3847,22 @@ function Acp({ isAdmin }) {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{systemSection === 'cli' && renderSystemRequirementsPanel()}
|
||||
{systemSection === 'ci' && (
|
||||
<>
|
||||
<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>
|
||||
<h5 className="mb-0">CI/CD</h5>
|
||||
</div>
|
||||
<div className="bb-acp-panel-body">
|
||||
{!systemStatus && (
|
||||
<p className="bb-muted mb-0">
|
||||
{t('system.not_found')}
|
||||
Placeholder: CI/CD pipelines, runner requirements, and deployment logs will
|
||||
live here.
|
||||
</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>
|
||||
{renderSystemRequirementsPanel()}
|
||||
</>
|
||||
)}
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
Reference in New Issue
Block a user