2025-12-23 14:47:37 +00:00
< ? php
/** @var bool $maintenance_enabled */
/** @var string $maintenance_message */
/** @var array $pending */
/** @var array $applied */
/** @var string $csrf_token */
/** @var string|null $next_pending */
/** @var array $migration_contents */
/** @var array $migration_records */
/** @var bool $test_migrations_exist */
/** @var array|null $migration_modal_result */
/** @var string|null $modal_to_open */
/** @var string|null $migration_error */
/** @var array $adminOverviewPills */
/** @var array $adminOverviewStatuses */
/** @var array $sectionState */
?>
< ? php
$preselectModalId = null ;
if ( ! empty ( $modal_to_open )) {
$preselectModalId = 'migrationModal' . md5 ( $modal_to_open );
}
2026-01-19 09:31:34 +00:00
?>
< style >
. tooltip {
font - size : 0.75 rem ;
}
. tooltip - inner {
font - size : 0.75 rem ;
max - width : 300 px ;
}
</ style >
< ? php
2025-12-23 14:47:37 +00:00
$tabs = $adminTabs ? ? [];
if ( empty ( $tabs )) {
$tabs = [
'overview' => [
'label' => 'Overview' ,
'url' => $sectionUrls [ 'overview' ] ? ? ( $app_root . '?page=admin' ),
'type' => 'core' ,
'hook' => null ,
'position' => 100 ,
],
];
}
$heroPills = [
[
'label' => 'Maintenance' ,
'value' => $maintenance_enabled ? 'enabled' : 'off' ,
'icon' => 'fas fa-power-off' ,
'tone' => $maintenance_enabled ? 'danger' : 'success' ,
],
[
'label' => 'Migrations' ,
'value' => count ( $pending ) . ' pending' ,
'icon' => 'fas fa-database' ,
'tone' => empty ( $pending ) ? 'neutral' : 'warning' ,
],
];
if ( ! empty ( $adminOverviewPills ) && is_array ( $adminOverviewPills )) {
foreach ( $adminOverviewPills as $pill ) {
if ( ! is_array ( $pill )) {
continue ;
}
$heroPills [] = [
'label' => ( string )( $pill [ 'label' ] ? ? 'Status' ),
'value' => ( string )( $pill [ 'value' ] ? ? '' ),
'icon' => ( string )( $pill [ 'icon' ] ? ? 'fas fa-info-circle' ),
'tone' => ( string )( $pill [ 'tone' ] ? ? 'info' ),
];
}
}
$statusItems = [
[
'label' => 'Maintenance mode' ,
'description' => $maintenance_enabled ? 'Live site shows downtime banner.' : 'Visitors see the normal experience.' ,
'value' => $maintenance_enabled ? 'ON' : 'OFF' ,
'tone' => $maintenance_enabled ? 'warning' : 'success' ,
],
[
'label' => 'Schema migrations' ,
'description' => empty ( $pending ) ? 'Database matches code.' : 'Pending updates detected.' ,
'value' => count ( $pending ) . ' pending' ,
'tone' => empty ( $pending ) ? 'success' : 'warning' ,
],
];
if ( ! empty ( $adminOverviewStatuses ) && is_array ( $adminOverviewStatuses )) {
foreach ( $adminOverviewStatuses as $status ) {
if ( ! is_array ( $status )) {
continue ;
}
$statusItems [] = [
'label' => ( string )( $status [ 'label' ] ? ? 'Status' ),
'description' => ( string )( $status [ 'description' ] ? ? '' ),
'value' => ( string )( $status [ 'value' ] ? ? '' ),
'tone' => ( string )( $status [ 'tone' ] ? ? 'info' ),
];
}
}
?>
< section class = " tm-hero " >
< div class = " tm-hero-card tm-hero-card--admin " >
< div class = " tm-hero-body " >
< div class = " tm-hero-heading " >
< h1 class = " tm-hero-title " > Admin control center </ h1 >
< p class = " tm-hero-subtitle " >
Centralized administration dashboard for system - wide management .
</ p >
</ div >
< div class = " tm-hero-meta tm-hero-meta--stacked " >
< ? php foreach ( $heroPills as $pill ) :
$toneClass = 'pill-' . preg_replace ( '/[^a-z0-9_-]/i' , '' , $pill [ 'tone' ] ? ? 'info' );
?>
< div class = " tm-hero-pill <?= htmlspecialchars( $toneClass ) ?> " >
< i class = " <?= htmlspecialchars( $pill['icon'] ) ?> " ></ i >
< ? = htmlspecialchars ( $pill [ 'label' ]) ?> <?= htmlspecialchars($pill['value']) ?>
</ div >
< ? php endforeach ; ?>
</ div >
</ div >
< div class = " tm-hero-actions " >
< a class = " btn btn-primary tm-directory-cta " href = " <?= htmlspecialchars( $app_root ) ?>?page=dashboard " >
< i class = " fas fa-arrow-left " ></ i > Back to dashboard
</ a >
</ div >
</ div >
</ section >
< section class = " tm-admin tm-admin--dashboard " >
< div class = " tm-admin-tabs " role = " tablist " >
< ? php foreach ( $tabs as $sectionKey => $tabMeta ) :
$isActive = $activeSection === $sectionKey ;
$tabUrl = $tabMeta [ 'url' ] ? ? ( $sectionUrls [ $sectionKey ] ? ? ( $app_root . '?page=admin§ion=' . urlencode ( $sectionKey )));
?>
< a class = " tm-admin-tab-button <?= $isActive ? 'active' : '' ?> "
href = " <?= htmlspecialchars( $tabUrl ) ?> "
role = " tab "
aria - selected = " <?= $isActive ? 'true' : 'false' ?> "
aria - controls = " tm-admin-tab-<?= htmlspecialchars( $sectionKey ) ?> " >
< ? = htmlspecialchars ( $tabMeta [ 'label' ] ? ? ucfirst ( $sectionKey )) ?>
</ a >
< ? php endforeach ; ?>
</ div >
< ? php foreach ( $tabs as $sectionKey => $tabMeta ) :
$panelUrl = $tabMeta [ 'url' ] ? ? ( $sectionUrls [ $sectionKey ] ? ? ( $app_root . '?page=admin§ion=' . urlencode ( $sectionKey )));
$isActive = $activeSection === $sectionKey ;
?>
< div class = " tm-admin-tab-panel <?= $isActive ? 'active' : '' ?> " id = " tm-admin-tab-<?= htmlspecialchars( $sectionKey ) ?> " role = " tabpanel " >
< ? php if (( $tabMeta [ 'type' ] ? ? 'core' ) === 'core' && $sectionKey === 'overview' ) : ?>
< div class = " tm-admin-grid tm-admin-grid--three " >
< article class = " tm-admin-card " >
< header >
< h2 class = " tm-admin-card-title " > Current status </ h2 >
< p class = " tm-admin-card-subtitle " > High - level signals that require your attention .</ p >
</ header >
< ul class = " tm-admin-status-list " >
< ? php foreach ( $statusItems as $status ) :
$statusTone = 'status-' . preg_replace ( '/[^a-z0-9_-]/i' , '' , $status [ 'tone' ] ? ? 'info' );
?>
< li class = " <?= htmlspecialchars( $statusTone ) ?> " >
< div >
< strong >< ? = htmlspecialchars ( $status [ 'label' ]) ?> </strong>
< ? php if ( ! empty ( $status [ 'description' ])) : ?>
< p >< ? = htmlspecialchars ( $status [ 'description' ]) ?> </p>
< ? php endif ; ?>
</ div >
< span class = " tm-admin-status-value <?= htmlspecialchars( $statusTone ) ?> " >
< ? = htmlspecialchars ( $status [ 'value' ]) ?>
</ span >
</ li >
< ? php endforeach ; ?>
</ ul >
</ article >
< article class = " tm-admin-card " >
< header >
< h2 class = " tm-admin-card-title " > Maintenance </ h2 >
< p class = " tm-admin-card-subtitle " > Toggle maintenance or update visitor message .</ p >
</ header >
< form method = " post " class = " tm-admin-controls " >
< input type = " hidden " name = " csrf_token " value = " <?= htmlspecialchars( $csrf_token ) ?> " >
< input type = " hidden " name = " action " value = " maintenance_on " >
< input type = " hidden " name = " section " value = " overview " >
< label for = " maintenance_message_overview " class = " form-label " > Maintenance message </ label >
< input type = " text "
id = " maintenance_message_overview "
name = " maintenance_message "
class = " form-control tm-admin-message-input "
value = " <?= htmlspecialchars( $maintenance_message ) ?> "
placeholder = " Custom message. Default is 'Please try again later.' " >
< div class = " tm-admin-inline-actions " >
< button type = " submit " class = " btn btn-warning " < ? = $maintenance_enabled ? 'disabled' : '' ?> >Enable</button>
< button type = " button " class = " btn btn-outline-secondary " < ? = $maintenance_enabled ? '' : 'disabled' ?>
onclick = " document.getElementById('maintenance-disable-form-overview').submit(); " > Disable </ button >
</ div >
</ form >
< form method = " post " id = " maintenance-disable-form-overview " class = " d-none " >
< input type = " hidden " name = " csrf_token " value = " <?= htmlspecialchars( $csrf_token ) ?> " >
< input type = " hidden " name = " action " value = " maintenance_off " >
< input type = " hidden " name = " section " value = " overview " >
</ form >
</ article >
< article class = " tm-admin-card " >
< header >
< h2 class = " tm-admin-card-title " > Next migration </ h2 >
< p class = " tm-admin-card-subtitle " > Peek at what will run when you apply updates .</ p >
</ header >
< ? php if ( $next_pending ) : ?>
< p class = " text-muted mb-2 " > Next : < strong >< ? = htmlspecialchars ( $next_pending ) ?> </strong></p>
< button class = " btn btn-outline-primary btn-sm " data - toggle = " modal " data - target = " #migrationModal<?= md5( $next_pending ) ?> " >
View SQL
</ button >
< ? php else : ?>
< p class = " tm-admin-empty " > No migrations queued .</ p >
< ? php endif ; ?>
< hr >
< form method = " post " class = " tm-confirm " data - confirm = " Apply all pending migrations? " >
< input type = " hidden " name = " csrf_token " value = " <?= htmlspecialchars( $csrf_token ) ?> " >
< input type = " hidden " name = " action " value = " migrate_up " >
< input type = " hidden " name = " section " value = " overview " >
< button type = " submit " class = " btn btn-danger w-100 " < ? = empty ( $pending ) ? 'disabled' : '' ?> >Apply all pending</button>
</ form >
</ article >
</ div >
< ? php elseif (( $tabMeta [ 'type' ] ? ? 'core' ) === 'core' && $sectionKey === 'maintenance' ) : ?>
< div class = " tm-admin-grid " >
< article class = " tm-admin-card " >
< header >
< div >
< h2 class = " tm-admin-card-title " > Maintenance mode </ h2 >
< p class = " tm-admin-card-subtitle " > Let your users know when maintenance is in progress .</ p >
</ div >
< span class = " tm-hero-pill <?= $maintenance_enabled ? 'pill-danger' : 'pill-neutral' ?> " >
< ? = $maintenance_enabled ? 'ENABLED' : 'DISABLED' ?>
</ span >
</ header >
< div class = " tm-admin-section " >
< p class = " tm-admin-section-title " > Message </ p >
< form method = " post " class = " tm-admin-controls " >
< input type = " hidden " name = " csrf_token " value = " <?= htmlspecialchars( $csrf_token ) ?> " >
< input type = " hidden " name = " action " value = " maintenance_on " >
< textarea id = " maintenance_message "
name = " maintenance_message "
class = " form-control tm-admin-message-input "
rows = " 3 "
placeholder = " Custom message. Default is 'Please try again later.' " >< ? = htmlspecialchars ( $maintenance_message ) ?> </textarea>
< div class = " tm-admin-inline-actions " >
< button type = " submit " class = " btn btn-warning " < ? = $maintenance_enabled ? 'disabled' : '' ?> >Enable maintenance</button>
< button type = " button " class = " btn btn-outline-secondary " < ? = $maintenance_enabled ? '' : 'disabled' ?> onclick="document.getElementById('maintenance-disable-form').submit();">Disable maintenance</button>
</ div >
</ form >
</ div >
< form method = " post " id = " maintenance-disable-form " class = " d-none " >
< input type = " hidden " name = " csrf_token " value = " <?= htmlspecialchars( $csrf_token ) ?> " >
< input type = " hidden " name = " action " value = " maintenance_off " >
< input type = " hidden " name = " section " value = " maintenance " >
</ form >
</ article >
</ div >
< ? php elseif (( $tabMeta [ 'type' ] ? ? 'core' ) === 'core' && $sectionKey === 'migrations' ) : ?>
< div class = " tm-admin-grid " >
< article class = " tm-admin-card tm-admin-card--migrations " >
< header >
< div >
< h2 class = " tm-admin-card-title " > Database migrations </ h2 >
< p class = " tm-admin-card-subtitle " > Review pending SQL and apply with confidence .</ p >
</ div >
</ header >
< ? php if ( ! empty ( $migration_error )) : ?>
< div class = " alert alert-danger " > Error : < ? = htmlspecialchars ( $migration_error ) ?> </div>
< ? php endif ; ?>
< div class = " tm-admin-test-tools " >
< p >< strong > Test migration tools </ strong ></ p >
< div class = " tm-admin-inline-actions " >
< form method = " post " >
< input type = " hidden " name = " csrf_token " value = " <?= htmlspecialchars( $csrf_token ) ?> " >
< input type = " hidden " name = " action " value = " create_test_migration " >
< input type = " hidden " name = " section " value = " migrations " >
< button type = " submit " class = " btn btn-outline-primary btn-sm " < ? = ! empty ( $test_migrations_exist ) ? 'disabled' : '' ?> >Create test migration</button>
</ form >
< form method = " post " >
< input type = " hidden " name = " csrf_token " value = " <?= htmlspecialchars( $csrf_token ) ?> " >
< input type = " hidden " name = " action " value = " clear_test_migrations " >
< input type = " hidden " name = " section " value = " migrations " >
< button type = " submit " class = " btn btn-outline-secondary btn-sm " < ? = empty ( $test_migrations_exist ) ? 'disabled' : '' ?> >Clear test migrations</button>
</ form >
</ div >
</ div >
< div class = " tm-admin-section " >
< div class = " d-flex justify-content-between align-items-center " >
< p class = " tm-admin-section-title mb-0 " > Pending migrations </ p >
< ? php if ( ! empty ( $next_pending )) : ?>
< span class = " badge bg-info text-dark " > Next : < ? = htmlspecialchars ( $next_pending ) ?> </span>
< ? php endif ; ?>
</ div >
< ul class = " tm-admin-list " >
< ? php if ( empty ( $pending )) : ?>
< li class = " tm-admin-empty " > No pending migrations </ li >
< ? php else : ?>
< ? php foreach ( $pending as $fname ) : ?>
< li >
< div class = " tm-admin-list-actions " >
< button type = " button " class = " btn btn-sm btn-outline-primary " data - toggle = " modal " data - target = " #migrationModal<?= md5( $fname ) ?> " > View </ button >
</ div >
< span >< ? = htmlspecialchars ( $fname ) ?> </span>
</ li >
< ? php endforeach ; ?>
< ? php endif ; ?>
</ ul >
</ div >
< div class = " tm-admin-section " >
< div class = " d-flex justify-content-between align-items-center " >
< p class = " tm-admin-section-title mb-0 " > Applied migrations </ p >
< span class = " badge bg-secondary " >< ? = count ( $applied ) ?> </span>
</ div >
< ul class = " tm-admin-list " >
< ? php if ( empty ( $applied )) : ?>
< li class = " tm-admin-empty " > No applied migrations yet </ li >
< ? php else : ?>
< ? php foreach ( $applied as $fname ) :
if ( strpos ( $fname , '_test_migration' ) !== false ) {
continue ;
}
?>
< li >
< div class = " tm-admin-list-actions " >
< button type = " button " class = " btn btn-sm btn-outline-secondary " data - toggle = " modal " data - target = " #migrationModal<?= md5( $fname ) ?> " > View </ button >
</ div >
< span >< ? = htmlspecialchars ( $fname ) ?> </span>
</ li >
< ? php endforeach ; ?>
< ? php endif ; ?>
</ ul >
</ div >
< form method = " post " class = " tm-confirm " data - confirm = " Apply all pending migrations? " >
< input type = " hidden " name = " csrf_token " value = " <?= htmlspecialchars( $csrf_token ) ?> " >
< input type = " hidden " name = " action " value = " migrate_up " >
< input type = " hidden " name = " section " value = " migrations " >
< button type = " submit " class = " btn btn-danger w-100 " < ? = empty ( $pending ) ? 'disabled' : '' ?> >Apply all pending</button>
</ form >
</ article >
</ div >
2025-12-23 16:39:07 +00:00
< ? php elseif (( $tabMeta [ 'type' ] ? ? 'core' ) === 'core' && $sectionKey === 'plugins' ) : ?>
< ? php
$pluginsState = $sectionState [ 'plugins' ] ? ? [];
$pluginsList = $pluginsState [ 'plugins' ] ? ? [];
$dependencyErrors = $pluginsState [ 'dependency_errors' ] ? ? [];
$totalPlugins = count ( $pluginsList );
$enabledPlugins = count ( array_filter ( $pluginsList , static function ( $plugin ) {
return ! empty ( $plugin [ 'enabled' ]);
}));
$issuesPlugins = count ( array_filter ( $pluginsList , static function ( $plugin ) {
return ! empty ( $plugin [ 'dependency_errors' ]) || ! $plugin [ 'loaded' ];
}));
?>
< div class = " tm-admin-grid " >
< article class = " tm-admin-card " >
< header >
< div >
< h2 class = " tm-admin-card-title " > Plugin overview </ h2 >
< p class = " tm-admin-card-subtitle " > Enable or disable functionality and review dependency health .</ p >
</ div >
< div class = " tm-hero-pill pill-primary " >
< ? = htmlspecialchars ( $enabledPlugins ) ?> / <?= htmlspecialchars($totalPlugins) ?> enabled
</ div >
</ header >
< ? php if ( ! empty ( $dependencyErrors )) : ?>
< div class = " alert alert-warning " >
< strong > Dependency issues detected .</ strong > Resolve the following before enabling affected plugins :
< ul class = " mb-0 mt-2 " >
< ? php foreach ( $dependencyErrors as $slug => $errors ) :
if ( empty ( $errors )) {
continue ;
}
?>
< li >< strong >< ? = htmlspecialchars ( $slug ) ?> :</strong> <?= htmlspecialchars(implode('; ', $errors)) ?></li>
< ? php endforeach ; ?>
</ ul >
</ div >
< ? php endif ; ?>
< ? php if ( empty ( $pluginsList )) : ?>
< p class = " tm-admin-empty mb-0 " > No plugins detected in the plugins directory .</ p >
< ? php else : ?>
< div class = " table-responsive " >
< table class = " table table-hover tm-admin-table " >
< thead >
< tr >
< th > Plugin </ th >
< th > Status </ th >
< th > Depends on </ th >
< th class = " text-right " > Actions </ th >
</ tr >
</ thead >
< tbody >
< ? php
$pluginIndex = $pluginsState [ 'plugin_index' ] ? ? [];
foreach ( $pluginsList as $plugin ) :
$missingDeps = $plugin [ 'missing_dependencies' ] ? ? [];
$depErrors = $plugin [ 'dependency_errors' ] ? ? [];
$dependents = $plugin [ 'dependents' ] ? ? [];
$enabledDependents = $plugin [ 'enabled_dependents' ] ? ? [];
$statusBadges = [];
$statusBadges [] = $plugin [ 'enabled' ]
? '<span class="badge text-uppercase" style="background-color:#198754;color:#fff;">Enabled</span>'
2026-01-20 09:38:17 +00:00
: '<span class="badge text-uppercase" style="background-color:#c6c6c6;color:#fff;">Disabled</span>' ;
2025-12-23 16:39:07 +00:00
if ( $plugin [ 'enabled' ] && empty ( $depErrors ) && $plugin [ 'loaded' ]) {
$statusBadges [] = '<span class="badge text-uppercase" style="background-color:#0dcaf0;color:#052c65;">Loaded</span>' ;
}
if ( ! empty ( $missingDeps ) || ! empty ( $depErrors )) {
$statusBadges [] = '<span class="badge text-uppercase" style="background-color:#ffc107;color:#212529;">Issues</span>' ;
}
?>
< tr >
< td >
< strong >< ? = htmlspecialchars ( $plugin [ 'name' ]) ?> </strong>
< ? php if ( ! empty ( $plugin [ 'version' ])) : ?>
< span class = " text-muted " > v < ? = htmlspecialchars ( $plugin [ 'version' ]) ?> </span>
< ? php endif ; ?>
< ? php if ( ! empty ( $plugin [ 'description' ])) : ?>
< p class = " tm-admin-muted mb-0 " >< ? = htmlspecialchars ( $plugin [ 'description' ]) ?> </p>
< ? php endif ; ?>
</ td >
< td >
< ? = implode ( ' ' , $statusBadges ) ?>
< ? php if ( ! empty ( $depErrors )) : ?>
< p class = " tm-admin-muted text-warning mb-0 " >< ? = htmlspecialchars ( implode ( ' ' , $depErrors )) ?> </p>
< ? php endif ; ?>
</ td >
< td >
< ? php if ( ! empty ( $plugin [ 'dependencies' ])) : ?>
< ul class = " tm-admin-inline-list " >
< ? php foreach ( $plugin [ 'dependencies' ] as $dep ) :
$depMeta = $pluginIndex [ $dep ] ? ? null ;
$depStatusBadge = '' ;
if ( $depMeta ) {
$depStatusBadge = $depMeta [ 'enabled' ]
? '<span class="badge" style="background-color:#198754;color:#fff;">OK</span>'
: '<span class="badge" style="background-color:#ffc107;color:#212529;">Off</span>' ;
if ( ! empty ( $depMeta [ 'dependency_errors' ])) {
$depStatusBadge = '<span class="badge" style="background-color:#dc3545;color:#fff;">Error</span>' ;
}
} elseif ( in_array ( $dep , $missingDeps , true )) {
$depStatusBadge = '<span class="badge" style="background-color:#dc3545;color:#fff;">Missing</span>' ;
}
?>
< li >
< ? = htmlspecialchars ( $dep ) ?>
< ? php if ( $depStatusBadge !== '' ) : ?>
2025-12-31 10:53:38 +00:00
< span class = " tm-admin-dep-status " >< ? = $depStatusBadge ?> </span>
2025-12-23 16:39:07 +00:00
< ? php endif ; ?>
</ li >
< ? php endforeach ; ?>
</ ul >
< ? php else : ?>
< span class = " text-muted " >-</ span >
< ? php endif ; ?>
</ td >
< td class = " text-right " >
2026-01-19 09:31:34 +00:00
< div class = " btn-group " role = " group " >
2025-12-23 16:39:07 +00:00
< ? php if ( $plugin [ 'enabled' ]) : ?>
2026-01-19 09:31:34 +00:00
< form method = " post " class = " d-inline " >
< input type = " hidden " name = " csrf_token " value = " <?= htmlspecialchars( $csrf_token ) ?> " >
< input type = " hidden " name = " section " value = " plugins " >
< input type = " hidden " name = " plugin " value = " <?= htmlspecialchars( $plugin['slug'] ) ?> " >
< input type = " hidden " name = " action " value = " plugin_disable " >
< ? php if ( $plugin [ 'can_disable' ]) : ?>
< span data - toggle = " tooltip " data - placement = " top " title = " Disable this plugin " >
< button type = " submit " class = " btn btn-sm btn-outline-danger " > Disable </ button >
</ span >
< ? php else : ?>
< span data - toggle = " tooltip " data - placement = " top "
title = " <?= htmlspecialchars('Cannot disable: ' . (count( $plugin['enabled_dependents'] ) > 0 ? 'Required by: ' . implode(', ', $plugin['enabled_dependents'] ) : 'Plugin has active dependents')) ?> " >
< button type = " button " class = " btn btn-sm btn-outline-danger " disabled > Disable </ button >
</ span >
< ? php endif ; ?>
</ form >
2025-12-23 16:39:07 +00:00
< ? php else : ?>
2026-01-19 09:31:34 +00:00
< form method = " post " class = " d-inline " >
< input type = " hidden " name = " csrf_token " value = " <?= htmlspecialchars( $csrf_token ) ?> " >
< input type = " hidden " name = " section " value = " plugins " >
< input type = " hidden " name = " plugin " value = " <?= htmlspecialchars( $plugin['slug'] ) ?> " >
< input type = " hidden " name = " action " value = " plugin_enable " >
< ? php if ( $plugin [ 'can_enable' ]) : ?>
< span data - toggle = " tooltip " data - placement = " top " title = " Enable this plugin " >
< button type = " submit " class = " btn btn-sm btn-outline-success " > Enable </ button >
</ span >
< ? php else : ?>
< span data - toggle = " tooltip " data - placement = " top "
title = " <?= htmlspecialchars('Cannot enable: ' . (count( $plugin['missing_dependencies'] ) > 0 ? 'Missing dependencies: ' . implode(', ', $plugin['missing_dependencies'] ) : 'Plugin has dependency issues')) ?> " >
< button type = " button " class = " btn btn-sm btn-outline-success " disabled > Enable </ button >
</ span >
< ? php endif ; ?>
</ form >
< ? php endif ; ?>
< ? php if ( file_exists ( $pluginAdminMap [ $plugin [ 'slug' ]][ 'path' ] . '/bootstrap.php' )) : ?>
2026-01-19 16:05:42 +00:00
< span data - toggle = " tooltip " data - placement = " top " title = " Check plugin health and status " style = " margin-left: 0.5rem; " >
2026-01-19 09:31:34 +00:00
< button type = " button " class = " btn btn-sm btn-outline-secondary " data - toggle = " modal " data - target = " #pluginCheckModal<?= htmlspecialchars( $plugin['slug'] ) ?> " > Check </ button >
</ span >
2025-12-23 16:39:07 +00:00
< ? php endif ; ?>
2026-01-19 09:31:34 +00:00
</ div >
2025-12-23 16:39:07 +00:00
</ td >
</ tr >
< ? php endforeach ; ?>
</ tbody >
</ table >
</ div >
< ? php endif ; ?>
</ article >
</ div >
2025-12-23 14:47:37 +00:00
< ? php elseif ( ! empty ( $tabMeta [ 'hook' ])) : ?>
< ? php
do_hook ( $tabMeta [ 'hook' ], [
'section' => $sectionKey ,
'active_section' => $activeSection ,
'app_root' => $app_root ,
'section_url' => $panelUrl ,
'section_urls' => $sectionUrls ? ? [],
'csrf_token' => $csrf_token ,
'state' => $sectionState [ $sectionKey ] ? ? [],
'section_state' => $sectionState ,
'db' => $db ? ? null ,
]);
?>
< ? php else : ?>
< article class = " tm-admin-card " >
< p class = " tm-admin-empty mb-0 " > No renderer available for this section .</ p >
</ article >
< ? php endif ; ?>
</ div >
< ? php endforeach ; ?>
</ section >
< ? php if ( ! empty ( $migration_contents )) :
foreach ( $migration_contents as $name => $content ) :
$modalId = 'migrationModal' . md5 ( $name );
$record = $migration_records [ $name ] ? ? null ;
$appliedAtRaw = $record [ 'applied_at' ] ? ? null ;
$appliedAtFormatted = null ;
if ( ! empty ( $appliedAtRaw )) {
$timestamp = strtotime ( $appliedAtRaw );
$appliedAtFormatted = $timestamp ? date ( 'M d, Y H:i' , $timestamp ) : $appliedAtRaw ;
}
$isModalNext = ( ! empty ( $next_pending ) && $next_pending === $name );
$modalResult = ( ! empty ( $migration_modal_result ) && ( $migration_modal_result [ 'name' ] ? ? '' ) === $name ) ? $migration_modal_result : null ;
?>
< div class = " modal fade " id = " <?= $modalId ?> " tabindex = " -1 " aria - labelledby = " <?= $modalId ?>Label " aria - hidden = " true " >
< div class = " modal-dialog modal-lg " >
< div class = " modal-content " >
< div class = " modal-header " >
< h5 class = " modal-title " id = " <?= $modalId ?>Label " >< ? = htmlspecialchars ( $name ) ?> </h5>
< button type = " button " class = " btn-close " data - dismiss = " modal " aria - label = " Close " ></ button >
</ div >
< div class = " modal-body p-0 " >
< pre class = " tm-admin-modal-code " >< code style = " border-radius: 0.5rem; " >< ? = htmlspecialchars ( $content ) ?> </code></pre>
</ div >
< div class = " modal-footer " >
< ? php if ( $isModalNext ) : ?>
< form method = " post " class = " me-auto tm-confirm " data - confirm = " Apply migration <?= htmlspecialchars( $name ) ?>? " >
< input type = " hidden " name = " csrf_token " value = " <?= htmlspecialchars( $csrf_token ) ?> " >
< input type = " hidden " name = " action " value = " migrate_apply_one " >
< input type = " hidden " name = " migration_name " value = " <?= htmlspecialchars( $name ) ?> " >
< input type = " hidden " name = " section " value = " migrations " >
< button type = " submit " class = " btn btn-danger " > Apply migration </ button >
</ form >
< ? php endif ; ?>
< ? php if ( $modalResult ) : ?>
< div class = " alert alert-<?= $modalResult['status'] === 'success' ? 'success' : 'info' ?> mb-0 small " >
< ? = htmlspecialchars ( $modalResult [ 'message' ]) ?>
</ div >
< ? php endif ; ?>
< ? php if ( $appliedAtFormatted ) : ?>
< div class = " tm-admin-modal-meta " >
< span class = " tm-admin-pill pill-success " >
< i class = " far fa-clock " ></ i >
Applied < ? = htmlspecialchars ( $appliedAtFormatted ) ?>
</ span >
</ div >
< ? php endif ; ?>
< button type = " button " class = " btn btn-secondary " data - dismiss = " modal " > Close </ button >
</ div >
</ div >
</ div >
</ div >
< ? php
endforeach ;
endif ; ?>
< form method = " post " id = " tm-admin-hidden-read-migration " class = " d-none " >
< input type = " hidden " name = " action " value = " read_migration " >
< input type = " hidden " name = " csrf_token " value = " <?= htmlspecialchars( $csrf_token ) ?> " >
< input type = " hidden " name = " filename " value = " " >
</ form >
2026-01-19 09:31:34 +00:00
<!-- Plugin Check Modals -->
< ? php foreach ( $pluginAdminList as $plugin ) : ?>
< ? php if ( file_exists ( $plugin [ 'path' ] . '/bootstrap.php' )) : ?>
< ? php
$modalId = 'pluginCheckModal' . htmlspecialchars ( $plugin [ 'slug' ]);
$checkResults = [];
2026-01-19 16:05:42 +00:00
2026-01-19 09:31:34 +00:00
try {
// Check plugin files exist
2026-01-19 09:40:36 +00:00
$migrationFiles = glob ( $plugin [ 'path' ] . '/migrations/*.sql' );
$hasMigration = ! empty ( $migrationFiles );
2026-01-19 16:05:42 +00:00
2026-01-19 09:31:34 +00:00
$checkResults [ 'files' ] = [
'manifest' => file_exists ( $plugin [ 'path' ] . '/plugin.json' ),
'bootstrap' => file_exists ( $plugin [ 'path' ] . '/bootstrap.php' ),
2026-01-19 16:05:42 +00:00
'helpers' => file_exists ( $plugin [ 'path' ] . '/helpers.php' ),
'controllers' => is_dir ( $plugin [ 'path' ] . '/controllers' ) && count ( glob ( $plugin [ 'path' ] . '/controllers/*.php' )) > 0 ,
2026-01-19 09:40:36 +00:00
'migration' => $hasMigration ,
2026-01-19 09:31:34 +00:00
];
2026-01-19 16:05:42 +00:00
2026-01-19 09:31:34 +00:00
// Check database tables
2026-01-19 10:08:17 +00:00
$db = \App\App :: db ();
2026-01-19 19:28:35 +00:00
$pluginOwnedTables = [];
$pluginReferencedTables = [];
if ( $db && method_exists ( $db , 'getConnection' )) {
$pdo = $db -> getConnection ();
$stmt = $pdo -> query ( " SHOW TABLES " );
2026-01-19 09:31:34 +00:00
$allTables = $stmt -> fetchAll ( PDO :: FETCH_COLUMN , 0 );
2026-01-19 16:05:42 +00:00
2026-01-19 09:40:36 +00:00
if ( $hasMigration ) {
foreach ( $migrationFiles as $migrationFile ) {
$migrationContent = file_get_contents ( $migrationFile );
2026-01-19 19:28:35 +00:00
// Extract tables created by this migration (plugin-owned)
if ( preg_match_all ( '/CREATE\s+TABLE(?:\s+IF\s+NOT\s+EXISTS)?\s+`?([a-zA-Z0-9_]+)`?/i' , $migrationContent , $matches )) {
foreach ( $matches [ 1 ] as $tableName ) {
if ( in_array ( $tableName , $allTables )) {
$pluginOwnedTables [] = $tableName ;
}
}
}
// Find all referenced tables (dependencies)
2026-01-19 09:40:36 +00:00
foreach ( $allTables as $table ) {
2026-01-19 19:28:35 +00:00
if ( strpos ( $migrationContent , $table ) !== false && ! in_array ( $table , $pluginOwnedTables )) {
$pluginReferencedTables [] = $table ;
2026-01-19 09:40:36 +00:00
}
2026-01-19 09:31:34 +00:00
}
}
2026-01-19 19:28:35 +00:00
$pluginOwnedTables = array_unique ( $pluginOwnedTables );
$pluginReferencedTables = array_unique ( $pluginReferencedTables );
2026-01-19 09:31:34 +00:00
}
}
2026-01-19 19:28:35 +00:00
$checkResults [ 'tables' ] = [
'owned' => $pluginOwnedTables ,
'referenced' => $pluginReferencedTables ,
];
2026-01-19 16:05:42 +00:00
// Check plugin functions and integrations
2026-01-19 09:31:34 +00:00
$bootstrapPath = $plugin [ 'path' ] . '/bootstrap.php' ;
if ( file_exists ( $bootstrapPath )) {
include_once $bootstrapPath ;
$migrationFunction = str_replace ( '-' , '_' , $plugin [ 'slug' ]) . '_ensure_tables' ;
2026-01-19 10:08:17 +00:00
$migrationTestResult = null ;
2026-01-19 16:05:42 +00:00
2026-01-19 10:08:17 +00:00
// Test migration function if it exists
if ( function_exists ( $migrationFunction )) {
try {
// Check if plugin tables already exist
$tablesExist = ! empty ( $pluginTables );
2026-01-19 16:05:42 +00:00
2026-01-19 10:08:17 +00:00
if ( $tablesExist ) {
$migrationTestResult = 'already installed' ;
} else {
// For plugins without tables, the function exists and is ready
$migrationTestResult = 'function ready (tables not installed)' ;
}
} catch ( Exception $e ) {
$migrationTestResult = 'error: ' . $e -> getMessage ();
}
} else {
$migrationTestResult = 'not applicable' ;
}
2026-01-19 16:05:42 +00:00
// Check route and hook registrations
$routePrefix = $plugin [ 'slug' ];
$hasRouteRegistration = isset ( $GLOBALS [ 'plugin_route_prefixes' ]) && isset ( $GLOBALS [ 'plugin_route_prefixes' ][ $routePrefix ]);
2026-01-19 09:31:34 +00:00
$checkResults [ 'functions' ] = [
'migration' => function_exists ( $migrationFunction ),
2026-01-19 10:08:17 +00:00
'migration_test' => $migrationTestResult ? : 'not applicable' ,
2026-01-19 16:05:42 +00:00
'route_registration' => $hasRouteRegistration ,
'hook_registration' => true , // If bootstrap loaded, assume hooks are registered
2026-01-19 09:31:34 +00:00
];
}
2026-01-19 16:05:42 +00:00
2026-01-19 09:31:34 +00:00
} catch ( Throwable $e ) {
$checkResults [ 'error' ] = $e -> getMessage ();
}
?>
< div class = " modal fade " id = " <?= $modalId ?> " tabindex = " -1 " aria - labelledby = " <?= $modalId ?>Label " aria - hidden = " true " >
< div class = " modal-dialog modal-lg " >
< div class = " modal-content " >
< div class = " modal-header " >
< h5 class = " modal-title " id = " <?= $modalId ?>Label " > Plugin Health Check : < ? = htmlspecialchars ( $plugin [ 'name' ]) ?> </h5>
< button type = " button " class = " btn-close " data - dismiss = " modal " aria - label = " Close " ></ button >
</ div >
< div class = " modal-body " >
< div class = " row " >
< div class = " col-md-6 " >
< div class = " card " >
< div class = " card-header " >
2026-01-19 16:05:42 +00:00
< h6 class = " card-title mb-0 " > Plugin Information </ h6 >
2026-01-19 09:31:34 +00:00
</ div >
< div class = " card-body " >
2026-01-19 16:05:42 +00:00
< div class = " small " >
< div class = " mb-1 " >< strong > Name :</ strong > < ? = htmlspecialchars ( $plugin [ 'name' ]) ?> </div>
< div class = " mb-1 " >< strong > Version :</ strong > < ? = htmlspecialchars ( $plugin [ 'version' ] ? ? 'N/A' ) ?> </div>
< div class = " mb-1 " >< strong > Enabled :</ strong > < span class = " badge bg-<?= $plugin['enabled'] ? 'success' : 'secondary' ?> " >< ? = $plugin [ 'enabled' ] ? 'Yes' : 'No' ?> </span></div>
< div class = " mb-1 " >< strong > Dependencies :</ strong > < ? = ! empty ( $plugin [ 'dependencies' ]) ? htmlspecialchars ( implode ( ', ' , $plugin [ 'dependencies' ])) : 'None' ?> </div>
</ div >
2026-01-19 09:31:34 +00:00
</ div >
</ div >
</ div >
< div class = " col-md-6 " >
< div class = " card " >
< div class = " card-header " >
< h6 class = " card-title mb-0 " > Functions Check </ h6 >
</ div >
< div class = " card-body " >
2026-01-19 10:08:17 +00:00
< ? php foreach ( $checkResults [ 'functions' ] ? ? [] as $func => $value ) : ?>
< ? php if ( $func === 'migration_test' ) : ?>
2026-01-19 16:05:42 +00:00
< ? php if ( $value !== 'not applicable' ) : ?>
2026-01-19 10:08:17 +00:00
< div class = " d-flex justify-content-between align-items-center mb-2 " >
< span > Migration Test </ span >
2026-01-19 16:05:42 +00:00
< ? php if ( $value === 'already installed' ) : ?>
2026-01-19 10:08:17 +00:00
< span class = " badge bg-info " > Already Installed </ span >
< ? php elseif ( strpos ( $value , 'error' ) === false ) : ?>
< span class = " badge bg-success " > Passed </ span >
< ? php else : ?>
< span class = " badge bg-danger " > Failed </ span >
< ? php endif ; ?>
</ div >
2026-01-19 16:05:42 +00:00
< ? php endif ; ?>
2026-01-19 10:08:17 +00:00
< ? php if ( $value === 'already installed' ) : ?>
< div class = " text-muted small mb-2 " > Plugin tables already exist - migration not needed </ div >
< ? php elseif ( strpos ( $value , 'error' ) !== false ) : ?>
< div class = " text-muted small mb-2 " >< ? = htmlspecialchars ( $value ) ?> </div>
< ? php endif ; ?>
2026-01-19 16:05:42 +00:00
< ? php elseif ( $func === 'route_registration' ) : ?>
< div class = " d-flex justify-content-between align-items-center mb-2 " >
< span > Route Registration </ span >
< span class = " badge bg-<?= $value ? 'success' : 'danger' ?> " >
< ? = $value ? 'Registered' : 'Not Registered' ?>
</ span >
</ div >
< ? php elseif ( $func === 'hook_registration' ) : ?>
< div class = " d-flex justify-content-between align-items-center mb-2 " >
< span > Hook Registration </ span >
< span class = " badge bg-<?= $value ? 'success' : 'danger' ?> " >
< ? = $value ? 'Active' : 'Inactive' ?>
</ span >
</ div >
2026-01-19 10:08:17 +00:00
< ? php elseif ( $func === 'migration' ) : ?>
< div class = " d-flex justify-content-between align-items-center mb-2 " >
2026-01-19 16:05:42 +00:00
< span >< ? = htmlspecialchars ( ucfirst ( $func )) ?> Function</span>
2026-01-19 10:08:17 +00:00
< span class = " badge bg-<?= $value ? 'success' : 'danger' ?> " >
< ? = $value ? 'Available' : 'Missing' ?>
</ span >
</ div >
< ? php endif ; ?>
2026-01-19 09:31:34 +00:00
< ? php endforeach ; ?>
</ div >
</ div >
</ div >
2026-01-19 16:05:42 +00:00
</ div >
< div class = " row mt-3 " >
2026-01-19 09:31:34 +00:00
< div class = " col-md-6 " >
< div class = " card " >
< div class = " card-header " >
2026-01-19 16:05:42 +00:00
< h6 class = " card-title mb-0 " > Database Tables </ h6 >
2026-01-19 09:31:34 +00:00
</ div >
< div class = " card-body " >
2026-01-19 19:28:35 +00:00
< ? php if ( ! empty ( $checkResults [ 'tables' ][ 'owned' ]) || ! empty ( $checkResults [ 'tables' ][ 'referenced' ])) : ?>
< ? php if ( ! empty ( $checkResults [ 'tables' ][ 'owned' ])) : ?>
< div class = " mb-3 " >
< strong class = " text-danger " > Plugin Tables ( removed on purge ) :</ strong >
< ? php foreach ( $checkResults [ 'tables' ][ 'owned' ] as $table ) : ?>
< div class = " d-flex justify-content-between align-items-center mb-2 mt-2 " >
< span >< i class = " fas fa-database text-danger " ></ i > < ? = htmlspecialchars ( $table ) ?> </span>
< span class = " badge bg-danger " > Owned </ span >
</ div >
< ? php endforeach ; ?>
2026-01-19 16:05:42 +00:00
</ div >
2026-01-19 19:28:35 +00:00
< ? php endif ; ?>
< ? php if ( ! empty ( $checkResults [ 'tables' ][ 'referenced' ])) : ?>
< div >
< strong class = " text-muted " > Referenced Tables ( dependencies ) :</ strong >
< ? php foreach ( $checkResults [ 'tables' ][ 'referenced' ] as $table ) : ?>
< div class = " d-flex justify-content-between align-items-center mb-2 mt-2 " >
< span >< i class = " fas fa-link text-muted " ></ i > < ? = htmlspecialchars ( $table ) ?> </span>
< span class = " badge bg-secondary " > Referenced </ span >
</ div >
< ? php endforeach ; ?>
</ div >
< ? php endif ; ?>
2026-01-19 16:05:42 +00:00
< ? php else : ?>
< p class = " text-muted mb-0 " >
< ? php if ( $checkResults [ 'files' ][ 'migration' ]) : ?>
Plugin has migration files but tables are not installed yet .
< ? php else : ?>
No plugin tables found .
< ? php endif ; ?>
</ p >
< ? php endif ; ?>
</ div >
</ div >
</ div >
< div class = " col-md-6 " >
< div class = " card " >
< div class = " card-header " >
< h6 class = " card-title mb-0 " > File System Check </ h6 >
</ div >
< div class = " card-body " >
< ? php foreach ( $checkResults [ 'files' ] ? ? [] as $file => $exists ) : ?>
< div class = " d-flex justify-content-between align-items-center mb-2 " >
< span >< ? = htmlspecialchars ( ucfirst ( $file )) ?> </span>
< span class = " badge bg-<?= $exists ? 'success' : 'danger' ?> " >
< ? = $exists ? 'Exists' : 'Missing' ?>
</ span >
</ div >
< ? php endforeach ; ?>
2026-01-19 09:31:34 +00:00
</ div >
</ div >
</ div >
</ div >
< ? php if ( isset ( $checkResults [ 'error' ])) : ?>
< div class = " alert alert-danger mt-3 " >
< strong > Error :</ strong > < ? = htmlspecialchars ( $checkResults [ 'error' ]) ?>
</ div >
< ? php endif ; ?>
</ div >
< div class = " modal-footer " >
2026-01-19 16:05:42 +00:00
< ? php if ( $plugin [ 'has_migration' ]) : ?>
< form method = " post " class = " d-inline " >
< input type = " hidden " name = " csrf_token " value = " <?= htmlspecialchars( $csrf_token ) ?> " >
< input type = " hidden " name = " section " value = " plugins " >
< input type = " hidden " name = " plugin " value = " <?= htmlspecialchars( $plugin['slug'] ) ?> " >
< input type = " hidden " name = " action " value = " plugin_install " >
< button type = " submit " class = " btn btn-primary " > Install plugin DB tables </ button >
</ form >
< form method = " post " class = " d-inline " >
< input type = " hidden " name = " csrf_token " value = " <?= htmlspecialchars( $csrf_token ) ?> " >
< input type = " hidden " name = " section " value = " plugins " >
< input type = " hidden " name = " plugin " value = " <?= htmlspecialchars( $plugin['slug'] ) ?> " >
< input type = " hidden " name = " action " value = " plugin_purge " >
< button type = " submit " class = " btn btn-warning " onclick = " return confirm('Are you sure? This will permanently delete all plugin data and tables!') " > Purge all plugin data </ button >
</ form >
< ? php endif ; ?>
2026-01-19 09:31:34 +00:00
< button type = " button " class = " btn btn-secondary " data - dismiss = " modal " > Close </ button >
</ div >
</ div >
</ div >
</ div >
< ? php endif ; ?>
< ? php endforeach ; ?>
< form method = " post " id = " tm-admin-hidden-read-migration " class = " d-none " >
< input type = " hidden " name = " action " value = " read_migration " >
< input type = " hidden " name = " csrf_token " value = " <?= htmlspecialchars( $csrf_token ) ?> " >
< input type = " hidden " name = " filename " value = " " >
</ form >
2025-12-23 14:47:37 +00:00
< script >
document . addEventListener ( 'DOMContentLoaded' , function () {
2026-01-19 09:31:34 +00:00
// Initialize tooltips
if ( typeof $ !== 'undefined' && $ . fn . tooltip ) {
$ ( '[data-toggle="tooltip"]' ) . tooltip ();
}
2026-01-19 16:05:42 +00:00
2025-12-23 14:47:37 +00:00
document . querySelectorAll ( 'form.tm-confirm' ) . forEach (( form ) => {
form . addEventListener ( 'submit' , ( event ) => {
const message = form . getAttribute ( 'data-confirm' ) || 'Are you sure?' ;
if ( ! confirm ( message )) {
event . preventDefault ();
}
});
});
const preselectModal = < ? = $preselectModalId ? '"#' . htmlspecialchars ( $preselectModalId ) . '"' : 'null' ?> ;
if ( preselectModal ) {
const el = document . querySelector ( preselectModal );
if ( el && window . $ ) {
window . $ ( el ) . modal ( 'show' );
}
}
});
</ script >