Update ACP system navigation
This commit is contained in:
@@ -1,5 +1,9 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
## 2026-02-08
|
## 2026-02-08
|
||||||
- Achieved 100% test coverage across the backend.
|
- Achieved 100% test coverage across the backend.
|
||||||
- Added comprehensive Feature and Unit tests for controllers, models, services, and console commands.
|
- Added comprehensive Feature and Unit tests for controllers, models, services, and console commands.
|
||||||
|
|||||||
@@ -96,6 +96,7 @@ function Acp({ isAdmin }) {
|
|||||||
const [systemStatus, setSystemStatus] = useState(null)
|
const [systemStatus, setSystemStatus] = useState(null)
|
||||||
const [systemLoading, setSystemLoading] = useState(false)
|
const [systemLoading, setSystemLoading] = useState(false)
|
||||||
const [systemError, setSystemError] = useState('')
|
const [systemError, setSystemError] = useState('')
|
||||||
|
const [systemSection, setSystemSection] = useState('info')
|
||||||
const [usersPage, setUsersPage] = useState(1)
|
const [usersPage, setUsersPage] = useState(1)
|
||||||
const [usersPerPage, setUsersPerPage] = useState(10)
|
const [usersPerPage, setUsersPerPage] = useState(10)
|
||||||
const [userSort, setUserSort] = useState({ columnId: 'name', direction: 'asc' })
|
const [userSort, setUserSort] = useState({ columnId: 'name', direction: 'asc' })
|
||||||
@@ -3507,254 +3508,352 @@ function Acp({ isAdmin }) {
|
|||||||
<Tab eventKey="system" title={t('acp.system')}>
|
<Tab eventKey="system" title={t('acp.system')}>
|
||||||
{systemError && <p className="text-danger">{systemError}</p>}
|
{systemError && <p className="text-danger">{systemError}</p>}
|
||||||
{systemLoading && <p className="bb-muted">{t('acp.loading')}</p>}
|
{systemLoading && <p className="bb-muted">{t('acp.loading')}</p>}
|
||||||
{!systemLoading && systemStatus && (
|
{!systemLoading && (
|
||||||
<div className="bb-acp-panel">
|
<Row className="g-4">
|
||||||
<div className="bb-acp-panel-header">
|
<Col lg={3} xl={2}>
|
||||||
<div className="d-flex align-items-center justify-content-between">
|
<div className="bb-acp-sidebar">
|
||||||
<h5 className="mb-0">{t('system.requirements')}</h5>
|
<div className="bb-acp-sidebar-section">
|
||||||
<Button
|
<div className="bb-acp-sidebar-title">{t('acp.system')}</div>
|
||||||
type="button"
|
<div className="list-group">
|
||||||
size="sm"
|
<button
|
||||||
variant="dark"
|
type="button"
|
||||||
onClick={loadSystemStatus}
|
className={`list-group-item list-group-item-action ${
|
||||||
disabled={systemLoading}
|
systemSection === 'info' ? 'is-active' : ''
|
||||||
>
|
}`}
|
||||||
{t('acp.refresh')}
|
onClick={() => setSystemSection('info')}
|
||||||
</Button>
|
>
|
||||||
|
Overview
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`list-group-item list-group-item-action ${
|
||||||
|
systemSection === 'insite' ? 'is-active' : ''
|
||||||
|
}`}
|
||||||
|
onClick={() => setSystemSection('insite')}
|
||||||
|
>
|
||||||
|
Live Update
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`list-group-item list-group-item-action ${
|
||||||
|
systemSection === 'cli' ? 'is-active' : ''
|
||||||
|
}`}
|
||||||
|
onClick={() => setSystemSection('cli')}
|
||||||
|
>
|
||||||
|
CLI
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`list-group-item list-group-item-action ${
|
||||||
|
systemSection === 'ci' ? 'is-active' : ''
|
||||||
|
}`}
|
||||||
|
onClick={() => setSystemSection('ci')}
|
||||||
|
>
|
||||||
|
CI/CD
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Col>
|
||||||
<div className="bb-acp-panel-body">
|
<Col lg={9} xl={10}>
|
||||||
<table className="bb-acp-stats-table">
|
{systemSection === 'info' && (
|
||||||
<thead>
|
<div className="bb-acp-panel">
|
||||||
<tr>
|
<div className="bb-acp-panel-header">
|
||||||
<th>{t('system.check')}</th>
|
<h5 className="mb-0">System overview</h5>
|
||||||
<th>{t('system.path')}</th>
|
</div>
|
||||||
<th>{t('system.min_version')}</th>
|
<div className="bb-acp-panel-body">
|
||||||
<th>{t('system.current_version')}</th>
|
<p className="bb-muted mb-0">
|
||||||
<th>{t('system.status')}</th>
|
Placeholder: summary, upgrade guidance, and environment health notes will
|
||||||
<th>{t('system.recheck')}</th>
|
live here.
|
||||||
</tr>
|
</p>
|
||||||
</thead>
|
</div>
|
||||||
<tbody>
|
</div>
|
||||||
<tr>
|
)}
|
||||||
<td>PHP</td>
|
{systemSection === 'insite' && (
|
||||||
<td className="bb-acp-stats-value">{systemStatus.php_selected_path || '—'}</td>
|
<div className="bb-acp-panel">
|
||||||
<td className="bb-acp-stats-value">
|
<div className="bb-acp-panel-header">
|
||||||
{systemStatus.min_versions?.php || '—'}
|
<h5 className="mb-0">Live Update</h5>
|
||||||
</td>
|
</div>
|
||||||
<td className="bb-acp-stats-value">
|
<div className="bb-acp-panel-body">
|
||||||
{systemStatus.php_selected_version || '—'}
|
<p className="bb-muted mb-0">
|
||||||
</td>
|
Placeholder: run a live update from inside the forum, with safety checks
|
||||||
<td className="bb-acp-stats-value">
|
and status details.
|
||||||
<StatusIcon
|
</p>
|
||||||
status={systemStatus.php_selected_ok ? 'ok' : 'bad'}
|
</div>
|
||||||
/>
|
</div>
|
||||||
</td>
|
)}
|
||||||
<td className="bb-acp-stats-value">
|
{systemSection === 'cli' && (
|
||||||
|
<div className="bb-acp-panel">
|
||||||
|
<div className="bb-acp-panel-header">
|
||||||
|
<h5 className="mb-0">CLI</h5>
|
||||||
|
</div>
|
||||||
|
<div className="bb-acp-panel-body">
|
||||||
|
<p className="bb-muted mb-0">
|
||||||
|
Placeholder: CLI upgrade commands and automation helpers will live here.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{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
|
<Button
|
||||||
|
type="button"
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="dark"
|
variant="dark"
|
||||||
onClick={loadSystemStatus}
|
onClick={loadSystemStatus}
|
||||||
disabled={systemLoading}
|
disabled={systemLoading}
|
||||||
>
|
>
|
||||||
{t('system.recheck')}
|
{t('acp.refresh')}
|
||||||
</Button>
|
</Button>
|
||||||
</td>
|
</div>
|
||||||
</tr>
|
</div>
|
||||||
<tr>
|
<div className="bb-acp-panel-body">
|
||||||
<td>Composer</td>
|
{!systemStatus && (
|
||||||
<td className="bb-acp-stats-value">
|
<p className="bb-muted mb-0">
|
||||||
{systemStatus.composer || t('system.not_found')}
|
{t('system.not_found')}
|
||||||
</td>
|
</p>
|
||||||
<td className="bb-acp-stats-value">
|
)}
|
||||||
{systemStatus.min_versions?.composer || '—'}
|
{systemStatus && (
|
||||||
</td>
|
<table className="bb-acp-stats-table">
|
||||||
<td className="bb-acp-stats-value">
|
<thead>
|
||||||
{systemStatus.composer_version || '—'}
|
<tr>
|
||||||
</td>
|
<th>{t('system.check')}</th>
|
||||||
<td className="bb-acp-stats-value">
|
<th>{t('system.path')}</th>
|
||||||
<StatusIcon status={systemStatus.composer ? 'ok' : 'bad'} />
|
<th>{t('system.min_version')}</th>
|
||||||
</td>
|
<th>{t('system.current_version')}</th>
|
||||||
<td className="bb-acp-stats-value">
|
<th>{t('system.status')}</th>
|
||||||
<Button
|
<th>{t('system.recheck')}</th>
|
||||||
size="sm"
|
</tr>
|
||||||
variant="dark"
|
</thead>
|
||||||
onClick={loadSystemStatus}
|
<tbody>
|
||||||
disabled={systemLoading}
|
<tr>
|
||||||
>
|
<td>PHP</td>
|
||||||
{t('system.recheck')}
|
<td className="bb-acp-stats-value">
|
||||||
</Button>
|
{systemStatus.php_selected_path || '—'}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
<td className="bb-acp-stats-value">
|
||||||
<tr>
|
{systemStatus.min_versions?.php || '—'}
|
||||||
<td>Node</td>
|
</td>
|
||||||
<td className="bb-acp-stats-value">
|
<td className="bb-acp-stats-value">
|
||||||
{systemStatus.node || t('system.not_found')}
|
{systemStatus.php_selected_version || '—'}
|
||||||
</td>
|
</td>
|
||||||
<td className="bb-acp-stats-value">
|
<td className="bb-acp-stats-value">
|
||||||
{systemStatus.min_versions?.node || '—'}
|
<StatusIcon
|
||||||
</td>
|
status={systemStatus.php_selected_ok ? 'ok' : 'bad'}
|
||||||
<td className="bb-acp-stats-value">
|
/>
|
||||||
{systemStatus.node_version || '—'}
|
</td>
|
||||||
</td>
|
<td className="bb-acp-stats-value">
|
||||||
<td className="bb-acp-stats-value">
|
<Button
|
||||||
<StatusIcon status={systemStatus.node ? 'ok' : 'bad'} />
|
size="sm"
|
||||||
</td>
|
variant="dark"
|
||||||
<td className="bb-acp-stats-value">
|
onClick={loadSystemStatus}
|
||||||
<Button
|
disabled={systemLoading}
|
||||||
size="sm"
|
>
|
||||||
variant="dark"
|
{t('system.recheck')}
|
||||||
onClick={loadSystemStatus}
|
</Button>
|
||||||
disabled={systemLoading}
|
</td>
|
||||||
>
|
</tr>
|
||||||
{t('system.recheck')}
|
<tr>
|
||||||
</Button>
|
<td>Composer</td>
|
||||||
</td>
|
<td className="bb-acp-stats-value">
|
||||||
</tr>
|
{systemStatus.composer || t('system.not_found')}
|
||||||
<tr>
|
</td>
|
||||||
<td>npm</td>
|
<td className="bb-acp-stats-value">
|
||||||
<td className="bb-acp-stats-value">
|
{systemStatus.min_versions?.composer || '—'}
|
||||||
{systemStatus.npm || t('system.not_found')}
|
</td>
|
||||||
</td>
|
<td className="bb-acp-stats-value">
|
||||||
<td className="bb-acp-stats-value">
|
{systemStatus.composer_version || '—'}
|
||||||
{systemStatus.min_versions?.npm || '—'}
|
</td>
|
||||||
</td>
|
<td className="bb-acp-stats-value">
|
||||||
<td className="bb-acp-stats-value">
|
<StatusIcon status={systemStatus.composer ? 'ok' : 'bad'} />
|
||||||
{systemStatus.npm_version || '—'}
|
</td>
|
||||||
</td>
|
<td className="bb-acp-stats-value">
|
||||||
<td className="bb-acp-stats-value">
|
<Button
|
||||||
<StatusIcon status={systemStatus.npm ? 'ok' : 'bad'} />
|
size="sm"
|
||||||
</td>
|
variant="dark"
|
||||||
<td className="bb-acp-stats-value">
|
onClick={loadSystemStatus}
|
||||||
<Button
|
disabled={systemLoading}
|
||||||
size="sm"
|
>
|
||||||
variant="dark"
|
{t('system.recheck')}
|
||||||
onClick={loadSystemStatus}
|
</Button>
|
||||||
disabled={systemLoading}
|
</td>
|
||||||
>
|
</tr>
|
||||||
{t('system.recheck')}
|
<tr>
|
||||||
</Button>
|
<td>Node</td>
|
||||||
</td>
|
<td className="bb-acp-stats-value">
|
||||||
</tr>
|
{systemStatus.node || t('system.not_found')}
|
||||||
<tr>
|
</td>
|
||||||
<td>tar</td>
|
<td className="bb-acp-stats-value">
|
||||||
<td className="bb-acp-stats-value">
|
{systemStatus.min_versions?.node || '—'}
|
||||||
{systemStatus.tar || t('system.not_found')}
|
</td>
|
||||||
</td>
|
<td className="bb-acp-stats-value">
|
||||||
<td className="bb-acp-stats-value">—</td>
|
{systemStatus.node_version || '—'}
|
||||||
<td className="bb-acp-stats-value">
|
</td>
|
||||||
{systemStatus.tar_version || '—'}
|
<td className="bb-acp-stats-value">
|
||||||
</td>
|
<StatusIcon status={systemStatus.node ? 'ok' : 'bad'} />
|
||||||
<td className="bb-acp-stats-value">
|
</td>
|
||||||
<StatusIcon status={systemStatus.tar ? 'ok' : 'bad'} />
|
<td className="bb-acp-stats-value">
|
||||||
</td>
|
<Button
|
||||||
<td className="bb-acp-stats-value">
|
size="sm"
|
||||||
<Button
|
variant="dark"
|
||||||
size="sm"
|
onClick={loadSystemStatus}
|
||||||
variant="dark"
|
disabled={systemLoading}
|
||||||
onClick={loadSystemStatus}
|
>
|
||||||
disabled={systemLoading}
|
{t('system.recheck')}
|
||||||
>
|
</Button>
|
||||||
{t('system.recheck')}
|
</td>
|
||||||
</Button>
|
</tr>
|
||||||
</td>
|
<tr>
|
||||||
</tr>
|
<td>npm</td>
|
||||||
<tr>
|
<td className="bb-acp-stats-value">
|
||||||
<td>rsync</td>
|
{systemStatus.npm || t('system.not_found')}
|
||||||
<td className="bb-acp-stats-value">
|
</td>
|
||||||
{systemStatus.rsync || t('system.not_found')}
|
<td className="bb-acp-stats-value">
|
||||||
</td>
|
{systemStatus.min_versions?.npm || '—'}
|
||||||
<td className="bb-acp-stats-value">—</td>
|
</td>
|
||||||
<td className="bb-acp-stats-value">
|
<td className="bb-acp-stats-value">
|
||||||
{systemStatus.rsync_version || '—'}
|
{systemStatus.npm_version || '—'}
|
||||||
</td>
|
</td>
|
||||||
<td className="bb-acp-stats-value">
|
<td className="bb-acp-stats-value">
|
||||||
<StatusIcon status={systemStatus.rsync ? 'ok' : 'bad'} />
|
<StatusIcon status={systemStatus.npm ? 'ok' : 'bad'} />
|
||||||
</td>
|
</td>
|
||||||
<td className="bb-acp-stats-value">
|
<td className="bb-acp-stats-value">
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="dark"
|
variant="dark"
|
||||||
onClick={loadSystemStatus}
|
onClick={loadSystemStatus}
|
||||||
disabled={systemLoading}
|
disabled={systemLoading}
|
||||||
>
|
>
|
||||||
{t('system.recheck')}
|
{t('system.recheck')}
|
||||||
</Button>
|
</Button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>proc_* functions</td>
|
<td>tar</td>
|
||||||
<td className="bb-acp-stats-value" colSpan={3}>
|
<td className="bb-acp-stats-value">
|
||||||
{systemStatus.proc_functions
|
{systemStatus.tar || t('system.not_found')}
|
||||||
? Object.entries(systemStatus.proc_functions)
|
</td>
|
||||||
.filter(([, ok]) => !ok)
|
<td className="bb-acp-stats-value">—</td>
|
||||||
.map(([name]) => name)
|
<td className="bb-acp-stats-value">
|
||||||
.join(', ')
|
{systemStatus.tar_version || '—'}
|
||||||
: '—'}
|
</td>
|
||||||
</td>
|
<td className="bb-acp-stats-value">
|
||||||
<td className="bb-acp-stats-value">
|
<StatusIcon status={systemStatus.tar ? 'ok' : 'bad'} />
|
||||||
<StatusIcon
|
</td>
|
||||||
status={
|
<td className="bb-acp-stats-value">
|
||||||
Boolean(systemStatus.proc_functions) &&
|
<Button
|
||||||
Object.values(systemStatus.proc_functions).every(Boolean)
|
size="sm"
|
||||||
? 'ok'
|
variant="dark"
|
||||||
: 'bad'
|
onClick={loadSystemStatus}
|
||||||
}
|
disabled={systemLoading}
|
||||||
/>
|
>
|
||||||
</td>
|
{t('system.recheck')}
|
||||||
<td className="bb-acp-stats-value">
|
</Button>
|
||||||
<Button
|
</td>
|
||||||
size="sm"
|
</tr>
|
||||||
variant="dark"
|
<tr>
|
||||||
onClick={loadSystemStatus}
|
<td>rsync</td>
|
||||||
disabled={systemLoading}
|
<td className="bb-acp-stats-value">
|
||||||
>
|
{systemStatus.rsync || t('system.not_found')}
|
||||||
{t('system.recheck')}
|
</td>
|
||||||
</Button>
|
<td className="bb-acp-stats-value">—</td>
|
||||||
</td>
|
<td className="bb-acp-stats-value">
|
||||||
</tr>
|
{systemStatus.rsync_version || '—'}
|
||||||
<tr>
|
</td>
|
||||||
<td>{t('system.storage_writable')}</td>
|
<td className="bb-acp-stats-value">
|
||||||
<td className="bb-acp-stats-value">storage/</td>
|
<StatusIcon status={systemStatus.rsync ? 'ok' : 'bad'} />
|
||||||
<td className="bb-acp-stats-value">—</td>
|
</td>
|
||||||
<td className="bb-acp-stats-value">—</td>
|
<td className="bb-acp-stats-value">
|
||||||
<td className="bb-acp-stats-value">
|
<Button
|
||||||
<StatusIcon status={systemStatus.storage_writable ? 'ok' : 'bad'} />
|
size="sm"
|
||||||
</td>
|
variant="dark"
|
||||||
<td className="bb-acp-stats-value">
|
onClick={loadSystemStatus}
|
||||||
<Button
|
disabled={systemLoading}
|
||||||
size="sm"
|
>
|
||||||
variant="dark"
|
{t('system.recheck')}
|
||||||
onClick={loadSystemStatus}
|
</Button>
|
||||||
disabled={systemLoading}
|
</td>
|
||||||
>
|
</tr>
|
||||||
{t('system.recheck')}
|
<tr>
|
||||||
</Button>
|
<td>proc_* functions</td>
|
||||||
</td>
|
<td className="bb-acp-stats-value" colSpan={3}>
|
||||||
</tr>
|
{systemStatus.proc_functions
|
||||||
<tr>
|
? Object.entries(systemStatus.proc_functions)
|
||||||
<td>{t('system.updates_writable')}</td>
|
.filter(([, ok]) => !ok)
|
||||||
<td className="bb-acp-stats-value">storage/app/updates</td>
|
.map(([name]) => name)
|
||||||
<td className="bb-acp-stats-value">—</td>
|
.join(', ')
|
||||||
<td className="bb-acp-stats-value">—</td>
|
: '—'}
|
||||||
<td className="bb-acp-stats-value">
|
</td>
|
||||||
<StatusIcon status={systemStatus.updates_writable ? 'ok' : 'bad'} />
|
<td className="bb-acp-stats-value">
|
||||||
</td>
|
<StatusIcon
|
||||||
<td className="bb-acp-stats-value">
|
status={
|
||||||
<Button
|
Boolean(systemStatus.proc_functions) &&
|
||||||
size="sm"
|
Object.values(systemStatus.proc_functions).every(Boolean)
|
||||||
variant="dark"
|
? 'ok'
|
||||||
onClick={loadSystemStatus}
|
: 'bad'
|
||||||
disabled={systemLoading}
|
}
|
||||||
>
|
/>
|
||||||
{t('system.recheck')}
|
</td>
|
||||||
</Button>
|
<td className="bb-acp-stats-value">
|
||||||
</td>
|
<Button
|
||||||
</tr>
|
size="sm"
|
||||||
</tbody>
|
variant="dark"
|
||||||
</table>
|
onClick={loadSystemStatus}
|
||||||
</div>
|
disabled={systemLoading}
|
||||||
</div>
|
>
|
||||||
|
{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>
|
||||||
|
)}
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
)}
|
)}
|
||||||
</Tab>
|
</Tab>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|||||||
Reference in New Issue
Block a user