907 lines
52 KiB
PHP
907 lines
52 KiB
PHP
<?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);
|
|
}
|
|
|
|
?>
|
|
<style>
|
|
.tooltip {
|
|
font-size: 0.75rem;
|
|
}
|
|
.tooltip-inner {
|
|
font-size: 0.75rem;
|
|
max-width: 300px;
|
|
}
|
|
</style>
|
|
|
|
<?php
|
|
$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>
|
|
<?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>'
|
|
: '<span class="badge text-uppercase" style="background-color:#c6c6c6;color:#fff;">Disabled</span>';
|
|
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 !== ''): ?>
|
|
<span class="tm-admin-dep-status"><?= $depStatusBadge ?></span>
|
|
<?php endif; ?>
|
|
</li>
|
|
<?php endforeach; ?>
|
|
</ul>
|
|
<?php else: ?>
|
|
<span class="text-muted">-</span>
|
|
<?php endif; ?>
|
|
</td>
|
|
<td class="text-right">
|
|
<div class="btn-group" role="group">
|
|
<?php if ($plugin['enabled']): ?>
|
|
<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>
|
|
<?php else: ?>
|
|
<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')): ?>
|
|
<span data-toggle="tooltip" data-placement="top" title="Check plugin health and status" style="margin-left: 0.5rem;">
|
|
<button type="button" class="btn btn-sm btn-outline-secondary" data-toggle="modal" data-target="#pluginCheckModal<?= htmlspecialchars($plugin['slug']) ?>">Check</button>
|
|
</span>
|
|
<?php endif; ?>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<?php endif; ?>
|
|
</article>
|
|
</div>
|
|
<?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>
|
|
|
|
<!-- Plugin Check Modals -->
|
|
<?php foreach ($pluginAdminList as $plugin): ?>
|
|
<?php if (file_exists($plugin['path'] . '/bootstrap.php')): ?>
|
|
<?php
|
|
$modalId = 'pluginCheckModal' . htmlspecialchars($plugin['slug']);
|
|
$checkResults = [];
|
|
|
|
try {
|
|
// Check plugin files exist
|
|
$migrationFiles = glob($plugin['path'] . '/migrations/*.sql');
|
|
$hasMigration = !empty($migrationFiles);
|
|
|
|
$checkResults['files'] = [
|
|
'manifest' => file_exists($plugin['path'] . '/plugin.json'),
|
|
'bootstrap' => file_exists($plugin['path'] . '/bootstrap.php'),
|
|
'helpers' => file_exists($plugin['path'] . '/helpers.php'),
|
|
'controllers' => is_dir($plugin['path'] . '/controllers') && count(glob($plugin['path'] . '/controllers/*.php')) > 0,
|
|
'migration' => $hasMigration,
|
|
];
|
|
|
|
// Check database tables
|
|
$db = \App\App::db();
|
|
$pluginOwnedTables = [];
|
|
$pluginReferencedTables = [];
|
|
if ($db && method_exists($db, 'getConnection')) {
|
|
$pdo = $db->getConnection();
|
|
$stmt = $pdo->query("SHOW TABLES");
|
|
$allTables = $stmt->fetchAll(PDO::FETCH_COLUMN, 0);
|
|
|
|
if ($hasMigration) {
|
|
foreach ($migrationFiles as $migrationFile) {
|
|
$migrationContent = file_get_contents($migrationFile);
|
|
|
|
// 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)
|
|
foreach ($allTables as $table) {
|
|
if (strpos($migrationContent, $table) !== false && !in_array($table, $pluginOwnedTables)) {
|
|
$pluginReferencedTables[] = $table;
|
|
}
|
|
}
|
|
}
|
|
$pluginOwnedTables = array_unique($pluginOwnedTables);
|
|
$pluginReferencedTables = array_unique($pluginReferencedTables);
|
|
}
|
|
}
|
|
$checkResults['tables'] = [
|
|
'owned' => $pluginOwnedTables,
|
|
'referenced' => $pluginReferencedTables,
|
|
];
|
|
|
|
// Check plugin functions and integrations
|
|
$bootstrapPath = $plugin['path'] . '/bootstrap.php';
|
|
if (file_exists($bootstrapPath)) {
|
|
include_once $bootstrapPath;
|
|
$migrationFunction = str_replace('-', '_', $plugin['slug']) . '_ensure_tables';
|
|
$migrationTestResult = null;
|
|
|
|
// Test migration function if it exists
|
|
if (function_exists($migrationFunction)) {
|
|
try {
|
|
// Check if plugin tables already exist
|
|
$tablesExist = !empty($pluginTables);
|
|
|
|
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';
|
|
}
|
|
|
|
// Check route and hook registrations
|
|
$routePrefix = $plugin['slug'];
|
|
$hasRouteRegistration = isset($GLOBALS['plugin_route_prefixes']) && isset($GLOBALS['plugin_route_prefixes'][$routePrefix]);
|
|
|
|
$checkResults['functions'] = [
|
|
'migration' => function_exists($migrationFunction),
|
|
'migration_test' => $migrationTestResult ?: 'not applicable',
|
|
'route_registration' => $hasRouteRegistration,
|
|
'hook_registration' => true, // If bootstrap loaded, assume hooks are registered
|
|
];
|
|
}
|
|
|
|
} 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">
|
|
<h6 class="card-title mb-0">Plugin Information</h6>
|
|
</div>
|
|
<div class="card-body">
|
|
<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>
|
|
</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">
|
|
<?php foreach ($checkResults['functions'] ?? [] as $func => $value): ?>
|
|
<?php if ($func === 'migration_test'): ?>
|
|
<?php if ($value !== 'not applicable'): ?>
|
|
<div class="d-flex justify-content-between align-items-center mb-2">
|
|
<span>Migration Test</span>
|
|
<?php if ($value === 'already installed'): ?>
|
|
<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>
|
|
<?php endif; ?>
|
|
<?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; ?>
|
|
<?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>
|
|
<?php elseif ($func === 'migration'): ?>
|
|
<div class="d-flex justify-content-between align-items-center mb-2">
|
|
<span><?= htmlspecialchars(ucfirst($func)) ?> Function</span>
|
|
<span class="badge bg-<?= $value ? 'success' : 'danger' ?>">
|
|
<?= $value ? 'Available' : 'Missing' ?>
|
|
</span>
|
|
</div>
|
|
<?php endif; ?>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="row mt-3">
|
|
<div class="col-md-6">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h6 class="card-title mb-0">Database Tables</h6>
|
|
</div>
|
|
<div class="card-body">
|
|
<?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; ?>
|
|
</div>
|
|
<?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; ?>
|
|
<?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; ?>
|
|
</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">
|
|
<?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; ?>
|
|
<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>
|
|
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function () {
|
|
// Initialize tooltips
|
|
if (typeof $ !== 'undefined' && $.fn.tooltip) {
|
|
$('[data-toggle="tooltip"]').tooltip();
|
|
}
|
|
|
|
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>
|