Update ACP system navigation
All checks were successful
CI/CD Pipeline / test (push) Successful in 2s
CI/CD Pipeline / deploy (push) Successful in 19s

This commit is contained in:
2026-02-10 19:52:24 +01:00
parent c67a3ec6d0
commit 95ebc7778d
2 changed files with 343 additions and 240 deletions

View File

@@ -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.

View File

@@ -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>