| Server IP : 213.255.246.8 / Your IP : 216.73.216.139 Web Server : Apache System : Linux dublin.stapolin.com 5.14.0-362.18.1.el9_3.x86_64 #1 SMP PREEMPT_DYNAMIC Mon Jan 29 07:05:48 EST 2024 x86_64 User : stapolin ( 1019) PHP Version : 8.4.24 Disable Function : exec,passthru,shell_exec,system MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : OFF | Sudo : ON | Pkexec : ON Directory : /home/stapolin/public_html/dash.stapolin.com/ |
Upload File : |
<?php
/**
* Website dashboard: per-site view with Dashboard, Backups, Themes, Plugins, Users, Uptime tabs.
* Uses the website sidebar (replaces main nav). Set $websiteForDashboard before including header.
*/
require_once __DIR__ . '/includes/functions.php';
$websiteId = isset($_GET['id']) ? (int)$_GET['id'] : 0;
$tab = isset($_GET['tab']) ? $_GET['tab'] : 'dashboard';
$validTabs = ['dashboard', 'backups', 'themes', 'plugins', 'users', 'uptime', 'history', 'extra'];
if (!in_array($tab, $validTabs)) {
$tab = 'dashboard';
}
if (!$websiteId) {
header('Location: updates.php');
exit;
}
try {
$db = getDb();
$website = $db->fetchOne(
"SELECT w.*, c.client_name
FROM st_websites w
LEFT JOIN st_clients c ON w.client_id = c.client_id
WHERE w.website_id = ?",
[$websiteId]
);
if (!$website) {
header('Location: updates.php');
exit;
}
// Latest uptime status for is_down
$latestUptime = $db->fetchOne("SELECT status FROM st_uptime_checks WHERE website_id = ? ORDER BY checked_at DESC LIMIT 1", [$websiteId]);
$website['is_down'] = $latestUptime && strtolower($latestUptime['status']) === 'down';
// Update status (has_updates, core/plugin/theme counts)
$updateStatus = null;
$updateStatusPlugins = [];
$updateStatusThemes = [];
$coreNeedsUpdate = 0;
try {
$updateStatus = $db->fetchOne("SELECT * FROM st_update_status WHERE website_id = ?", [$websiteId]);
if ($updateStatus) {
$coreNeedsUpdate = !empty($updateStatus['core_needs_update']) ? 1 : 0;
if (!empty($updateStatus['plugins_json'])) {
$decoded = json_decode($updateStatus['plugins_json'], true);
$updateStatusPlugins = is_array($decoded) ? array_filter($decoded, function ($p) { return !empty($p['needs_update']); }) : [];
}
if (!empty($updateStatus['themes_json'])) {
$decoded = json_decode($updateStatus['themes_json'], true);
$updateStatusThemes = is_array($decoded) ? array_filter($decoded, function ($t) { return !empty($t['needs_update']); }) : [];
}
}
} catch (Exception $e) {}
$website['has_updates'] = $coreNeedsUpdate || count($updateStatusPlugins) > 0 || count($updateStatusThemes) > 0;
// Site snapshot (installed plugins, themes, users)
$siteSnapshot = null;
$snapshotPlugins = [];
$snapshotThemes = [];
$snapshotUsers = [];
try {
$siteSnapshot = $db->fetchOne("SELECT * FROM st_site_snapshot WHERE website_id = ?", [$websiteId]);
if ($siteSnapshot) {
if (!empty($siteSnapshot['plugins_json'])) {
$decoded = json_decode($siteSnapshot['plugins_json'], true);
$snapshotPlugins = is_array($decoded) ? $decoded : [];
}
if (!empty($siteSnapshot['themes_json'])) {
$decoded = json_decode($siteSnapshot['themes_json'], true);
$snapshotThemes = is_array($decoded) ? $decoded : [];
}
if (!empty($siteSnapshot['users_json'])) {
$decoded = json_decode($siteSnapshot['users_json'], true);
$snapshotUsers = is_array($decoded) ? $decoded : [];
}
}
} catch (Exception $e) {}
// Last 5 backups
$lastBackups = [];
try {
$lastBackups = $db->fetchAll("SELECT * FROM st_backups WHERE website_id = ? ORDER BY modified DESC LIMIT 5", [$websiteId]);
} catch (Exception $e) {}
// All backups (for Backups tab)
$allBackups = [];
try {
$allBackups = $db->fetchAll("SELECT * FROM st_backups WHERE website_id = ? ORDER BY modified DESC", [$websiteId]);
} catch (Exception $e) {}
// Uptime chart data (7 days for dashboard card, 30 for Uptime tab)
$uptimeChartData7 = getUptimeChartData($websiteId, 7);
$uptimeChartData30 = getUptimeChartData($websiteId, 30);
// Update history (for History tab): from st_activity (Core, Plugins, Themes)
$updateHistory = [];
$updateHistoryUsers = [];
$filterType = isset($_GET['filter_type']) ? trim($_GET['filter_type']) : '';
$filterUser = isset($_GET['filter_user']) ? trim($_GET['filter_user']) : '';
if (in_array($filterType, ['', 'Core', 'Plugin', 'Theme'], true)) {
$params = [$websiteId];
$typeCond = '';
if ($filterType !== '') {
$typeCond = ' AND object_type = ?';
$params[] = $filterType === 'Plugin' ? 'Plugins' : ($filterType === 'Theme' ? 'Themes' : $filterType);
}
$userCond = '';
if ($filterUser !== '') {
$userCond = ' AND username = ?';
$params[] = $filterUser;
}
$sql = "SELECT object_name AS name, object_type AS type, COALESCE(action, '') AS action, hist_time, COALESCE(username, '') AS username
FROM st_activity
WHERE website_id = ? AND object_type IN ('Core','Plugins','Themes')" . $typeCond . $userCond . " ORDER BY hist_time DESC LIMIT 500";
$updateHistory = $db->fetchAll($sql, $params);
}
$updateHistoryUsers = $db->fetchAll(
"SELECT DISTINCT username FROM st_activity WHERE website_id = ? AND object_type IN ('Core','Plugins','Themes') AND username IS NOT NULL AND username != '' ORDER BY username",
[$websiteId]
);
// Extra work (for Extra tab)
$extraWork = [];
try {
$extraWork = $db->fetchAll("SELECT * FROM st_extra_work WHERE website_id = ? ORDER BY work_date DESC", [$websiteId]);
} catch (Exception $e) {}
$websiteForDashboard = $website;
$websiteDashboardTab = $tab;
$pageTitle = $website['website_name'] ?: $website['website_url'];
} catch (Exception $e) {
header('Location: updates.php');
exit;
}
require_once __DIR__ . '/includes/header.php';
$baseUrl = 'website-dashboard.php?id=' . $websiteId;
?>
<!-- Breadcrumb (in main content area) -->
<div class="flex items-center gap-2 text-sm text-muted-foreground mb-6">
<a href="updates.php" class="hover:text-foreground">Updates</a>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/></svg>
<span class="text-foreground"><?= sanitize($website['website_name'] ?: $website['website_url']) ?></span>
</div>
<?php if ($tab === 'dashboard'): ?>
<!-- Dashboard tab: Updates card (tabs), Uptime card, Last 5 backups, Analytics placeholder -->
<?php
$updateSubTab = isset($_GET['sub']) ? $_GET['sub'] : 'core';
if (!in_array($updateSubTab, ['core', 'plugins', 'themes'])) $updateSubTab = 'core';
?>
<div class="space-y-6">
<!-- Updates card with sub-tabs -->
<div class="card rounded-lg overflow-hidden">
<div class="border-b">
<nav class="flex -mb-px">
<a href="<?= $baseUrl ?>&sub=core" class="px-6 py-4 text-sm font-medium border-b-2 <?= $updateSubTab === 'core' ? 'border-primary text-primary' : 'border-transparent text-muted-foreground' ?> flex items-center gap-2">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/></svg>
Core <span class="badge <?= $coreNeedsUpdate ? 'badge-info' : 'badge-secondary' ?>"><?= $coreNeedsUpdate ?></span>
</a>
<a href="<?= $baseUrl ?>&sub=plugins" class="px-6 py-4 text-sm font-medium border-b-2 <?= $updateSubTab === 'plugins' ? 'border-primary text-primary' : 'border-transparent text-muted-foreground' ?> flex items-center gap-2">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 4a2 2 0 114 0v1a1 1 0 001 1h3a1 1 0 011 1v3a1 1 0 01-1 1h-1a2 2 0 100 4h1a1 1 0 011 1v3a1 1 0 01-1 1h-3a1 1 0 01-1-1v-1a2 2 0 10-4 0v1a1 1 0 01-1 1H7a1 1 0 01-1-1v-3a1 1 0 00-1-1H4a2 2 0 110-4h1a1 1 0 001-1V7a1 1 0 011-1h3a1 1 0 001-1V4z"/></svg>
Plugins <span class="badge <?= count($updateStatusPlugins) > 0 ? 'badge-success' : 'badge-secondary' ?>"><?= count($updateStatusPlugins) ?></span>
</a>
<a href="<?= $baseUrl ?>&sub=themes" class="px-6 py-4 text-sm font-medium border-b-2 <?= $updateSubTab === 'themes' ? 'border-primary text-primary' : 'border-transparent text-muted-foreground' ?> flex items-center gap-2">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 21a4 4 0 01-4-4V5a2 2 0 012-2h4a2 2 0 012 2v12a4 4 0 01-4 4zm0 0h12a2 2 0 002-2v-4a2 2 0 00-2-2h-2.343M11 7.343l1.657-1.657a2 2 0 012.828 0l2.829 2.829a2 2 0 010 2.828l-8.486 8.485M7 17h.01"/></svg>
Themes <span class="badge <?= count($updateStatusThemes) > 0 ? 'badge-warning' : 'badge-secondary' ?>"><?= count($updateStatusThemes) ?></span>
</a>
</nav>
</div>
<div class="p-6">
<?php if ($updateSubTab === 'core'): ?>
<?php if ($coreNeedsUpdate && $updateStatus): ?>
<p class="text-sm mb-3">WordPress core: <span class="font-mono"><?= sanitize($updateStatus['core_current'] ?? '—') ?></span> → <span class="font-mono"><?= sanitize($updateStatus['core_latest'] ?? '—') ?></span></p>
<button type="button" class="btn-primary px-3 py-1.5 text-sm font-medium rounded-md remote-update-btn" data-action="core" data-website-id="<?= (int)$websiteId ?>">Update WordPress</button>
<?php else: ?>
<p class="text-muted-foreground text-sm">WordPress core is up to date<?= $updateStatus && !empty($updateStatus['core_current']) ? ' (' . sanitize($updateStatus['core_current']) . ')' : '' ?>.</p>
<?php endif; ?>
<?php elseif ($updateSubTab === 'plugins'): ?>
<?php if (empty($updateStatusPlugins)): ?>
<p class="text-muted-foreground text-sm">All plugins are up to date.</p>
<?php else: ?>
<div class="space-y-2 mb-4">
<?php foreach ($updateStatusPlugins as $p): $pluginPath = $p['plugin'] ?? ''; $canUpdate = $pluginPath !== ''; ?>
<label class="flex items-center gap-2 text-sm <?= $canUpdate ? '' : 'opacity-75' ?>">
<?php if ($canUpdate): ?>
<input type="checkbox" class="remote-update-plugin-cb rounded border-border" value="<?= sanitize($pluginPath) ?>">
<?php endif; ?>
<span><?= sanitize($p['name'] ?? 'Plugin') ?> — <?= sanitize($p['current'] ?? '') ?> → <?= sanitize($p['latest'] ?? '') ?></span>
</label>
<?php endforeach; ?>
</div>
<div class="flex gap-2">
<button type="button" class="btn-secondary px-3 py-1.5 text-sm font-medium rounded-md remote-update-selected-btn" data-action="plugins" data-website-id="<?= (int)$websiteId ?>">Update selected</button>
<button type="button" class="btn-primary px-3 py-1.5 text-sm font-medium rounded-md remote-update-btn" data-action="plugins" data-website-id="<?= (int)$websiteId ?>" data-plugins="all">Update all plugins</button>
</div>
<?php endif; ?>
<?php else: ?>
<?php if (empty($updateStatusThemes)): ?>
<p class="text-muted-foreground text-sm">All themes are up to date.</p>
<?php else: ?>
<div class="space-y-2 mb-4">
<?php foreach ($updateStatusThemes as $t): $themeSlug = $t['slug'] ?? ''; $canUpdate = $themeSlug !== ''; ?>
<label class="flex items-center gap-2 text-sm <?= $canUpdate ? '' : 'opacity-75' ?>">
<?php if ($canUpdate): ?>
<input type="checkbox" class="remote-update-theme-cb rounded border-border" value="<?= sanitize($themeSlug) ?>">
<?php endif; ?>
<span><?= sanitize($t['name'] ?? 'Theme') ?> — <?= sanitize($t['current'] ?? '') ?> → <?= sanitize($t['latest'] ?? '') ?></span>
</label>
<?php endforeach; ?>
</div>
<div class="flex gap-2">
<button type="button" class="btn-secondary px-3 py-1.5 text-sm font-medium rounded-md remote-update-selected-btn" data-action="themes" data-website-id="<?= (int)$websiteId ?>">Update selected</button>
<button type="button" class="btn-primary px-3 py-1.5 text-sm font-medium rounded-md remote-update-btn" data-action="themes" data-website-id="<?= (int)$websiteId ?>" data-themes="all">Update all themes</button>
</div>
<?php endif; ?>
<?php endif; ?>
</div>
</div>
<!-- Uptime and Backups side by side -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- Uptime card (7 days) -->
<div class="card rounded-lg p-6">
<h3 class="text-lg font-semibold mb-4">Uptime (last 7 days)</h3>
<?php
$hasUptime7 = !empty($uptimeChartData7['labels']) && (array_filter($uptimeChartData7['uptime_pct']) !== [] || array_filter($uptimeChartData7['response_time_ms']) !== []);
?>
<?php if (!$hasUptime7): ?>
<p class="text-muted-foreground text-sm">No uptime data yet. Run the uptime cron to collect data.</p>
<?php else: ?>
<div class="space-y-4">
<div><p class="text-sm text-muted-foreground mb-2">Uptime %</p><div class="h-48"><canvas id="dashboard-uptime-chart-7" aria-label="Uptime 7d"></canvas></div></div>
<div><p class="text-sm text-muted-foreground mb-2">Response time (ms)</p><div class="h-48"><canvas id="dashboard-response-chart-7" aria-label="Response 7d"></canvas></div></div>
</div>
<script>window.__uptimeChartData7 = <?= json_encode($uptimeChartData7) ?>;</script>
<?php endif; ?>
</div>
<!-- Last 5 backups -->
<div class="card rounded-lg p-6">
<h3 class="text-lg font-semibold mb-4">Last 5 backups</h3>
<?php if (empty($lastBackups)): ?>
<p class="text-muted-foreground text-sm">No backups recorded yet.</p>
<?php else: ?>
<ul class="space-y-2 text-sm">
<?php foreach ($lastBackups as $b): ?>
<li class="flex items-center justify-between gap-2">
<span class="truncate"><?= sanitize($b['filename'] ?? $b['destination'] ?? 'Backup') ?></span>
<span class="text-muted-foreground flex-shrink-0"><?= !empty($b['modified']) ? formatDateTime($b['modified']) : '' ?></span>
</li>
<?php endforeach; ?>
</ul>
<p class="mt-3"><a href="<?= $baseUrl ?>&tab=backups" class="text-sm text-primary hover:underline">View all backups</a></p>
<?php endif; ?>
</div>
</div>
<!-- Analytics placeholder -->
<div class="card rounded-lg p-6">
<h3 class="text-lg font-semibold mb-4">Analytics</h3>
<p class="text-muted-foreground text-sm">Analytics will be available in a future update.</p>
</div>
</div>
<?php if ($hasUptime7): ?>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
<script>
(function(){
var d = window.__uptimeChartData7;
if (!d || !d.labels || !d.labels.length) return;
var opts = { responsive: true, maintainAspectRatio: false };
new Chart(document.getElementById('dashboard-uptime-chart-7'), { type: 'bar', data: { labels: d.labels, datasets: [{ label: 'Uptime %', data: d.uptime_pct, backgroundColor: 'rgba(16, 185, 129, 0.6)', borderColor: 'rgb(16, 185, 129)', borderWidth: 1 }] }, options: Object.assign({}, opts, { scales: { y: { min: 0, max: 100 } } }) });
new Chart(document.getElementById('dashboard-response-chart-7'), { type: 'line', data: { labels: d.labels, datasets: [{ label: 'Response (ms)', data: d.response_time_ms, borderColor: 'rgb(59, 130, 246)', backgroundColor: 'rgba(59, 130, 246, 0.1)', fill: true, tension: 0.2 }] }, options: Object.assign({}, opts, { scales: { y: { min: 0 } } }) });
})();
</script>
<?php endif; ?>
<?php elseif ($tab === 'backups'): ?>
<div class="card rounded-lg overflow-hidden">
<h3 class="text-lg font-semibold p-6 border-b">Backups</h3>
<?php if (empty($allBackups)): ?>
<div class="p-6 text-muted-foreground text-sm">No backups recorded for this site.</div>
<?php else: ?>
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead class="bg-muted/50"><tr><th class="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase">Destination</th><th class="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase">Filename</th><th class="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase">Status</th><th class="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase">Modified</th></tr></thead>
<tbody class="divide-y">
<?php foreach ($allBackups as $b): ?>
<tr><td class="px-6 py-3"><?= sanitize($b['destination'] ?? '') ?></td><td class="px-6 py-3"><?= sanitize($b['filename'] ?? '') ?></td><td class="px-6 py-3"><?= sanitize($b['status'] ?? '') ?></td><td class="px-6 py-3"><?= !empty($b['modified']) ? formatDateTime($b['modified']) : '' ?></td></tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
<?php elseif ($tab === 'themes'): ?>
<?php
$themeSlugByName = [];
foreach ($updateStatusThemes as $t) {
$n = $t['name'] ?? '';
if ($n !== '' && isset($t['slug'])) $themeSlugByName[$n] = $t['slug'];
}
$themeSlugsWithUpdate = array_column(array_filter($updateStatusThemes, function ($t) { return !empty($t['slug']); }), 'slug');
$activeThemes = [];
$inactiveThemes = [];
foreach ($snapshotThemes as $t) {
$active = is_array($t) && !empty($t['active']);
if ($active) $activeThemes[] = $t; else $inactiveThemes[] = $t;
}
$themesSub = isset($_GET['themes_list']) && $_GET['themes_list'] === 'inactive' ? 'inactive' : 'active';
?>
<div class="card rounded-lg overflow-hidden">
<h3 class="text-lg font-semibold p-6 border-b">Themes</h3>
<?php if (empty($snapshotThemes)): ?>
<div class="p-6 text-muted-foreground text-sm">No theme data yet. Site snapshot is sent by the Stapolin Activity plugin.</div>
<?php else: ?>
<div class="border-b px-6 flex gap-4">
<a href="<?= $baseUrl ?>&tab=themes&themes_list=active" class="py-3 text-sm font-medium border-b-2 <?= $themesSub === 'active' ? 'border-primary text-primary' : 'border-transparent text-muted-foreground hover:text-foreground' ?>">Active <span class="badge badge-secondary ml-1"><?= count($activeThemes) ?></span></a>
<a href="<?= $baseUrl ?>&tab=themes&themes_list=inactive" class="py-3 text-sm font-medium border-b-2 <?= $themesSub === 'inactive' ? 'border-primary text-primary' : 'border-transparent text-muted-foreground hover:text-foreground' ?>">Inactive <span class="badge badge-secondary ml-1"><?= count($inactiveThemes) ?></span></a>
</div>
<?php if ($themesSub === 'active'): ?>
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead class="bg-muted/50"><tr><th class="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase">Name</th><th class="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase">Version</th><th class="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase">Update</th><th class="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase">Actions</th></tr></thead>
<tbody class="divide-y">
<?php foreach ($activeThemes as $t):
$name = is_array($t) ? ($t['name'] ?? $t['Name'] ?? '') : '';
$ver = is_array($t) ? ($t['version'] ?? $t['Version'] ?? '') : '';
$slug = $themeSlugByName[$name] ?? ($t['slug'] ?? '');
$needs = $slug !== '' && in_array($slug, $themeSlugsWithUpdate);
?>
<tr>
<td class="px-6 py-3"><?= sanitize($name) ?></td>
<td class="px-6 py-3 font-mono"><?= sanitize($ver) ?></td>
<td class="px-6 py-3"><?= $needs ? '<span class="badge badge-warning">Available</span>' : '—' ?></td>
<td class="px-6 py-3"><?= $needs ? '<button type="button" class="remote-update-one-btn btn-primary px-2 py-1 text-xs font-medium rounded-md" data-action="themes" data-website-id="' . (int)$websiteId . '" data-themes="' . htmlspecialchars($slug, ENT_QUOTES, 'UTF-8') . '">Update</button>' : '<span class="text-muted-foreground text-xs">Currently active</span>' ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php else: ?>
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead class="bg-muted/50"><tr><th class="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase">Name</th><th class="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase">Version</th><th class="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase">Update</th><th class="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase">Actions</th></tr></thead>
<tbody class="divide-y">
<?php foreach ($inactiveThemes as $t):
$name = is_array($t) ? ($t['name'] ?? $t['Name'] ?? '') : '';
$ver = is_array($t) ? ($t['version'] ?? $t['Version'] ?? '') : '';
$slug = $themeSlugByName[$name] ?? ($t['slug'] ?? '');
$needs = $slug !== '' && in_array($slug, $themeSlugsWithUpdate);
$canActivate = $slug !== '';
?>
<tr>
<td class="px-6 py-3"><?= sanitize($name) ?></td>
<td class="px-6 py-3 font-mono"><?= sanitize($ver) ?></td>
<td class="px-6 py-3"><?= $needs ? '<span class="badge badge-warning">Available</span>' : '—' ?></td>
<td class="px-6 py-3 flex gap-2">
<?php if ($canActivate): ?><button type="button" class="plugin-theme-action-btn btn-primary px-2 py-1 text-xs font-medium rounded-md" data-type="theme" data-action="activate" data-website-id="<?= (int)$websiteId ?>" data-theme="<?= htmlspecialchars($slug, ENT_QUOTES, 'UTF-8') ?>">Activate</button><?php endif; ?>
<?php if ($canActivate): ?><button type="button" class="plugin-theme-action-btn border border-border px-2 py-1 text-xs font-medium rounded-md text-red-600 hover:bg-red-50" data-type="theme" data-action="delete" data-website-id="<?= (int)$websiteId ?>" data-theme="<?= htmlspecialchars($slug, ENT_QUOTES, 'UTF-8') ?>">Delete</button><?php endif; ?>
<?php if ($needs): ?><button type="button" class="remote-update-one-btn btn-primary px-2 py-1 text-xs font-medium rounded-md" data-action="themes" data-website-id="<?= (int)$websiteId ?>" data-themes="<?= htmlspecialchars($slug, ENT_QUOTES, 'UTF-8') ?>">Update</button><?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
<?php endif; ?>
</div>
<?php elseif ($tab === 'plugins'): ?>
<?php
$pluginPathByName = [];
foreach ($updateStatusPlugins as $p) {
$n = $p['name'] ?? '';
if ($n !== '' && isset($p['plugin'])) $pluginPathByName[$n] = $p['plugin'];
}
$pluginPathsWithUpdate = array_column(array_filter($updateStatusPlugins, function ($p) { return !empty($p['plugin']); }), 'plugin');
$activePlugins = [];
$inactivePlugins = [];
foreach ($snapshotPlugins as $p) {
$active = is_array($p) && !empty($p['active']);
if ($active) $activePlugins[] = $p; else $inactivePlugins[] = $p;
}
$pluginsSub = isset($_GET['plugins_list']) && $_GET['plugins_list'] === 'inactive' ? 'inactive' : 'active';
?>
<div class="card rounded-lg overflow-hidden">
<h3 class="text-lg font-semibold p-6 border-b">Plugins</h3>
<?php if (empty($snapshotPlugins)): ?>
<div class="p-6 text-muted-foreground text-sm">No plugin data yet. Site snapshot is sent by the Stapolin Activity plugin.</div>
<?php else: ?>
<div class="border-b px-6 flex gap-4">
<a href="<?= $baseUrl ?>&tab=plugins&plugins_list=active" class="py-3 text-sm font-medium border-b-2 <?= $pluginsSub === 'active' ? 'border-primary text-primary' : 'border-transparent text-muted-foreground hover:text-foreground' ?>">Active <span class="badge badge-secondary ml-1"><?= count($activePlugins) ?></span></a>
<a href="<?= $baseUrl ?>&tab=plugins&plugins_list=inactive" class="py-3 text-sm font-medium border-b-2 <?= $pluginsSub === 'inactive' ? 'border-primary text-primary' : 'border-transparent text-muted-foreground hover:text-foreground' ?>">Inactive <span class="badge badge-secondary ml-1"><?= count($inactivePlugins) ?></span></a>
</div>
<?php if ($pluginsSub === 'active'): ?>
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead class="bg-muted/50"><tr><th class="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase">Name</th><th class="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase">Version</th><th class="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase">Update</th><th class="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase">Actions</th></tr></thead>
<tbody class="divide-y">
<?php foreach ($activePlugins as $p):
$name = is_array($p) ? ($p['name'] ?? $p['Name'] ?? '') : '';
$ver = is_array($p) ? ($p['version'] ?? $p['Version'] ?? '') : '';
$pluginPath = $pluginPathByName[$name] ?? ($p['plugin'] ?? '');
$needs = $pluginPath !== '' && in_array($pluginPath, $pluginPathsWithUpdate);
$canDeactivate = $pluginPath !== '';
?>
<tr>
<td class="px-6 py-3"><?= sanitize($name) ?></td>
<td class="px-6 py-3 font-mono"><?= sanitize($ver) ?></td>
<td class="px-6 py-3"><?= $needs ? '<span class="badge badge-success">Available</span>' : '—' ?></td>
<td class="px-6 py-3 flex gap-2">
<?php if ($canDeactivate): ?><button type="button" class="plugin-theme-action-btn border border-border px-2 py-1 text-xs font-medium rounded-md hover:bg-muted" data-type="plugin" data-action="deactivate" data-website-id="<?= (int)$websiteId ?>" data-plugin="<?= htmlspecialchars($pluginPath, ENT_QUOTES, 'UTF-8') ?>">Deactivate</button><?php endif; ?>
<?php if ($needs): ?><button type="button" class="remote-update-one-btn btn-primary px-2 py-1 text-xs font-medium rounded-md" data-action="plugins" data-website-id="<?= (int)$websiteId ?>" data-plugins="<?= htmlspecialchars($pluginPath, ENT_QUOTES, 'UTF-8') ?>">Update</button><?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php else: ?>
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead class="bg-muted/50"><tr><th class="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase">Name</th><th class="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase">Version</th><th class="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase">Update</th><th class="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase">Actions</th></tr></thead>
<tbody class="divide-y">
<?php foreach ($inactivePlugins as $p):
$name = is_array($p) ? ($p['name'] ?? $p['Name'] ?? '') : '';
$ver = is_array($p) ? ($p['version'] ?? $p['Version'] ?? '') : '';
$pluginPath = $pluginPathByName[$name] ?? ($p['plugin'] ?? '');
$needs = $pluginPath !== '' && in_array($pluginPath, $pluginPathsWithUpdate);
$canActivate = $pluginPath !== '';
?>
<tr>
<td class="px-6 py-3"><?= sanitize($name) ?></td>
<td class="px-6 py-3 font-mono"><?= sanitize($ver) ?></td>
<td class="px-6 py-3"><?= $needs ? '<span class="badge badge-success">Available</span>' : '—' ?></td>
<td class="px-6 py-3 flex gap-2">
<?php if ($canActivate): ?><button type="button" class="plugin-theme-action-btn btn-primary px-2 py-1 text-xs font-medium rounded-md" data-type="plugin" data-action="activate" data-website-id="<?= (int)$websiteId ?>" data-plugin="<?= htmlspecialchars($pluginPath, ENT_QUOTES, 'UTF-8') ?>">Activate</button><?php endif; ?>
<?php if ($canActivate): ?><button type="button" class="plugin-theme-action-btn border border-border px-2 py-1 text-xs font-medium rounded-md text-red-600 hover:bg-red-50" data-type="plugin" data-action="delete" data-website-id="<?= (int)$websiteId ?>" data-plugin="<?= htmlspecialchars($pluginPath, ENT_QUOTES, 'UTF-8') ?>">Delete</button><?php endif; ?>
<?php if ($needs): ?><button type="button" class="remote-update-one-btn btn-primary px-2 py-1 text-xs font-medium rounded-md" data-action="plugins" data-website-id="<?= (int)$websiteId ?>" data-plugins="<?= htmlspecialchars($pluginPath, ENT_QUOTES, 'UTF-8') ?>">Update</button><?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
<?php endif; ?>
</div>
<?php elseif ($tab === 'users'): ?>
<div class="card rounded-lg overflow-hidden">
<h3 class="text-lg font-semibold p-6 border-b">Users</h3>
<?php if (empty($snapshotUsers)): ?>
<div class="p-6 text-muted-foreground text-sm">No user data yet. Site snapshot is sent by the Stapolin Activity plugin.</div>
<?php else: ?>
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead class="bg-muted/50"><tr><th class="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase">Username</th><th class="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase">Email</th><th class="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase">Role</th><th class="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase">Registered</th><th class="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase">Last signed in</th></tr></thead>
<tbody class="divide-y">
<?php foreach ($snapshotUsers as $u):
$username = is_array($u) ? ($u['username'] ?? $u['login'] ?? $u['user_login'] ?? '') : '';
$email = is_array($u) ? ($u['email'] ?? $u['user_email'] ?? '') : '';
$role = is_array($u) ? ($u['role'] ?? $u['roles'] ?? '') : '';
if (is_array($role)) $role = implode(', ', $role);
$registered = is_array($u) ? ($u['registered'] ?? $u['registered_date'] ?? '') : '';
$lastLogin = is_array($u) ? ($u['last_login'] ?? '') : '';
?>
<tr><td class="px-6 py-3"><?= sanitize($username) ?></td><td class="px-6 py-3"><?= sanitize($email) ?></td><td class="px-6 py-3"><?= sanitize($role) ?></td><td class="px-6 py-3"><?= $registered ? formatDateTime($registered) : '—' ?></td><td class="px-6 py-3"><?= $lastLogin ? formatDateTime($lastLogin) : '—' ?></td></tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
<?php elseif ($tab === 'uptime'): ?>
<div class="card rounded-lg p-6">
<h3 class="text-lg font-semibold mb-4">Uptime monitoring (last 30 days)</h3>
<?php
$hasUptime30 = !empty($uptimeChartData30['labels']) && (array_filter($uptimeChartData30['uptime_pct']) !== [] || array_filter($uptimeChartData30['response_time_ms']) !== []);
?>
<?php if (!$hasUptime30): ?>
<p class="text-muted-foreground text-sm">No uptime data yet. Run the uptime cron to collect data.</p>
<?php else: ?>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div><p class="text-sm text-muted-foreground mb-2">Uptime %</p><div class="h-64"><canvas id="uptime-chart-30" aria-label="Uptime 30d"></canvas></div></div>
<div><p class="text-sm text-muted-foreground mb-2">Response time (ms)</p><div class="h-64"><canvas id="response-chart-30" aria-label="Response 30d"></canvas></div></div>
</div>
<script>window.__uptimeChartData30 = <?= json_encode($uptimeChartData30) ?>;</script>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
<script>
(function(){
var d = window.__uptimeChartData30;
if (!d || !d.labels || !d.labels.length) return;
var opts = { responsive: true, maintainAspectRatio: false };
new Chart(document.getElementById('uptime-chart-30'), { type: 'bar', data: { labels: d.labels, datasets: [{ label: 'Uptime %', data: d.uptime_pct, backgroundColor: 'rgba(16, 185, 129, 0.6)', borderColor: 'rgb(16, 185, 129)', borderWidth: 1 }] }, options: Object.assign({}, opts, { scales: { y: { min: 0, max: 100 } } }) });
new Chart(document.getElementById('response-chart-30'), { type: 'line', data: { labels: d.labels, datasets: [{ label: 'Response (ms)', data: d.response_time_ms, borderColor: 'rgb(59, 130, 246)', backgroundColor: 'rgba(59, 130, 246, 0.1)', fill: true, tension: 0.2 }] }, options: Object.assign({}, opts, { scales: { y: { min: 0 } } }) });
})();
</script>
<?php endif; ?>
</div>
<?php elseif ($tab === 'history'): ?>
<?php
$historyBaseUrl = $baseUrl . '&tab=history';
$historyFilterType = isset($_GET['filter_type']) ? trim($_GET['filter_type']) : '';
$historyFilterUser = isset($_GET['filter_user']) ? trim($_GET['filter_user']) : '';
?>
<div class="card rounded-lg overflow-hidden">
<h3 class="text-lg font-semibold p-6 border-b">Update history</h3>
<div class="p-6 border-b bg-muted/30 flex flex-wrap items-center gap-4">
<form method="get" action="website-dashboard.php" class="flex flex-wrap items-center gap-4">
<input type="hidden" name="id" value="<?= (int)$websiteId ?>">
<input type="hidden" name="tab" value="history">
<label class="flex items-center gap-2 text-sm">
<span class="text-muted-foreground">Type</span>
<select name="filter_type" class="rounded-md border border-border bg-background px-3 py-1.5 text-sm" onchange="this.form.submit()">
<option value=""<?= $historyFilterType === '' ? ' selected' : '' ?>>All</option>
<option value="Core"<?= $historyFilterType === 'Core' ? ' selected' : '' ?>>Core</option>
<option value="Plugin"<?= $historyFilterType === 'Plugin' ? ' selected' : '' ?>>Plugin</option>
<option value="Theme"<?= $historyFilterType === 'Theme' ? ' selected' : '' ?>>Theme</option>
</select>
</label>
<label class="flex items-center gap-2 text-sm">
<span class="text-muted-foreground">User</span>
<select name="filter_user" class="rounded-md border border-border bg-background px-3 py-1.5 text-sm" onchange="this.form.submit()">
<option value=""<?= $historyFilterUser === '' ? ' selected' : '' ?>>All</option>
<?php foreach ($updateHistoryUsers as $u): $un = $u['username'] ?? ''; ?>
<option value="<?= sanitize($un) ?>"<?= $historyFilterUser === $un ? ' selected' : '' ?>><?= sanitize($un) ?></option>
<?php endforeach; ?>
</select>
</label>
<?php if ($historyFilterType !== '' || $historyFilterUser !== ''): ?>
<a href="<?= $historyBaseUrl ?>" class="text-sm text-primary hover:underline">Clear filters</a>
<?php endif; ?>
</form>
</div>
<?php if (empty($updateHistory)): ?>
<div class="p-6 text-muted-foreground text-sm">No update history recorded for this site. History is sent by the Stapolin Activity plugin when updates are run.</div>
<?php else: ?>
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead class="bg-muted/50">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase">Name / Description</th>
<th class="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase">Type</th>
<th class="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase">Action</th>
<th class="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase">Date</th>
<th class="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase">User</th>
</tr>
</thead>
<tbody class="divide-y">
<?php foreach ($updateHistory as $row):
$actionLabel = trim($row['action'] ?? '');
if ($actionLabel === '') $actionLabel = '—';
else $actionLabel = ucfirst(strtolower($actionLabel));
?>
<tr>
<td class="px-6 py-3"><?= sanitize($row['name'] ?? '') ?></td>
<td class="px-6 py-3"><span class="badge <?= ($row['type'] ?? '') === 'Core' ? 'badge-info' : (($row['type'] ?? '') === 'Plugin' ? 'badge-success' : 'badge-warning') ?>"><?= sanitize($row['type'] ?? '') ?></span></td>
<td class="px-6 py-3"><?= sanitize($actionLabel) ?></td>
<td class="px-6 py-3 text-muted-foreground"><?= !empty($row['hist_time']) ? formatDateTime($row['hist_time']) : '—' ?></td>
<td class="px-6 py-3"><?= sanitize($row['username'] ?? '—') ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<p class="p-4 text-xs text-muted-foreground border-t">Showing up to 500 most recent updates.</p>
<?php endif; ?>
</div>
<?php elseif ($tab === 'extra'): ?>
<div class="card rounded-lg overflow-hidden">
<h3 class="text-lg font-semibold p-6 border-b">Extra work</h3>
<div class="p-6 border-b bg-muted/30">
<form id="extra-work-form" class="grid grid-cols-1 md:grid-cols-12 gap-4 items-end">
<input type="hidden" name="website_id" value="<?= (int)$websiteId ?>">
<div class="md:col-span-2">
<label for="extra_work_date" class="block text-sm font-medium text-muted-foreground mb-1">Date</label>
<input type="date" id="extra_work_date" name="work_date" required class="w-full px-3 py-2 rounded-md text-sm border border-border bg-background" value="<?= date('Y-m-d') ?>">
</div>
<div class="md:col-span-5">
<label for="extra_work_description" class="block text-sm font-medium text-muted-foreground mb-1">Description</label>
<input type="text" id="extra_work_description" name="description" required class="w-full px-3 py-2 rounded-md text-sm border border-border bg-background" placeholder="Describe the work performed...">
</div>
<div class="md:col-span-2">
<label for="extra_work_time" class="block text-sm font-medium text-muted-foreground mb-1">Time (minutes)</label>
<input type="number" id="extra_work_time" name="time_spent" required min="1" class="w-full px-3 py-2 rounded-md text-sm border border-border bg-background" placeholder="e.g. 30">
</div>
<div class="md:col-span-3">
<button type="submit" id="extra-work-submit" class="btn-primary px-4 py-2 rounded-md text-sm font-medium w-full md:w-auto">Add entry</button>
</div>
</form>
</div>
<?php if (empty($extraWork)): ?>
<div class="p-6 text-muted-foreground text-sm">No extra work recorded for this site yet. Use the form above to add an entry.</div>
<?php else: ?>
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead class="bg-muted/50">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase">Description</th>
<th class="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase">Date</th>
<th class="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase">Time</th>
</tr>
</thead>
<tbody class="divide-y" id="extra-work-tbody">
<?php foreach ($extraWork as $work): ?>
<tr>
<td class="px-6 py-3"><?= sanitize($work['description'] ?? '') ?></td>
<td class="px-6 py-3 text-muted-foreground whitespace-nowrap"><?= !empty($work['work_date']) ? formatDate($work['work_date']) : '—' ?></td>
<td class="px-6 py-3 font-mono whitespace-nowrap"><?= isset($work['time_spent']) ? formatDuration((int)$work['time_spent']) : '—' ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
<?php endif; ?>
<script>
(function() {
function doRemoteUpdate(websiteId, action, plugins, themes, btn) {
var origText = btn ? btn.textContent : '';
if (btn) { btn.disabled = true; btn.textContent = 'Updating…'; }
var payload = { website_id: websiteId, action: action };
if (plugins) payload.plugins = Array.isArray(plugins) ? plugins : [plugins];
if (themes) payload.themes = Array.isArray(themes) ? themes : [themes];
fetch('api/remote-update.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
})
.then(function(r) { return r.json(); })
.then(function(data) {
if (btn) { btn.disabled = false; btn.textContent = origText; }
if (data.success) {
if (typeof showToast === 'function') showToast(data.message || 'Update completed');
else alert(data.message || 'Update completed');
// Refresh update status from site so lists no longer show the updated item as needing update
fetch('api/refresh-update-status.php?website_id=' + websiteId)
.then(function(r) { return r.json(); })
.then(function() { setTimeout(function() { window.location.reload(); }, 800); })
.catch(function() { setTimeout(function() { window.location.reload(); }, 1500); });
} else {
if (typeof showToast === 'function') showToast(data.error || 'Update failed', 'error');
else alert(data.error || 'Update failed');
}
})
.catch(function() {
if (btn) { btn.disabled = false; btn.textContent = origText; }
if (typeof showToast === 'function') showToast('Request failed', 'error');
else alert('Request failed');
});
}
document.querySelectorAll('.remote-update-btn').forEach(function(btn) {
btn.addEventListener('click', function() {
var action = btn.getAttribute('data-action');
var websiteId = btn.getAttribute('data-website-id');
var plugins = btn.getAttribute('data-plugins');
var themes = btn.getAttribute('data-themes');
doRemoteUpdate(
parseInt(websiteId, 10),
action,
plugins === 'all' ? ['all'] : null,
themes === 'all' ? ['all'] : null,
btn
);
});
});
document.querySelectorAll('.remote-update-selected-btn').forEach(function(btn) {
btn.addEventListener('click', function() {
var action = btn.getAttribute('data-action');
var websiteId = parseInt(btn.getAttribute('data-website-id'), 10);
var list = action === 'plugins'
? document.querySelectorAll('.remote-update-plugin-cb:checked')
: document.querySelectorAll('.remote-update-theme-cb:checked');
var values = [].map.call(list, function(cb) { return cb.value; });
if (values.length === 0) {
if (typeof showToast === 'function') showToast('Select at least one item', 'error');
else alert('Select at least one item');
return;
}
doRemoteUpdate(websiteId, action, action === 'plugins' ? values : null, action === 'themes' ? values : null, btn);
});
});
document.querySelectorAll('.remote-update-one-btn').forEach(function(btn) {
btn.addEventListener('click', function() {
var action = btn.getAttribute('data-action');
var websiteId = parseInt(btn.getAttribute('data-website-id'), 10);
var plugins = btn.getAttribute('data-plugins');
var themes = btn.getAttribute('data-themes');
doRemoteUpdate(websiteId, action, action === 'plugins' ? [plugins] : null, action === 'themes' ? [themes] : null, btn);
});
});
function doPluginThemeAction(websiteId, type, action, plugin, theme, btn) {
var origText = btn ? btn.textContent : '';
if (btn) { btn.disabled = true; btn.textContent = '…'; }
var payload = { website_id: websiteId, type: type, action: action };
if (type === 'plugin') payload.plugin = plugin; else payload.theme = theme;
fetch('api/remote-plugin-theme-action.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
})
.then(function(r) { return r.json(); })
.then(function(data) {
if (btn) { btn.disabled = false; btn.textContent = origText; }
if (data.success) {
if (typeof showToast === 'function') showToast(data.message || 'Done');
else alert(data.message || 'Done');
fetch('api/refresh-snapshot.php?website_id=' + websiteId)
.then(function(r) { return r.json(); })
.then(function() { setTimeout(function() { window.location.reload(); }, 800); })
.catch(function() { setTimeout(function() { window.location.reload(); }, 1500); });
} else {
if (typeof showToast === 'function') showToast(data.error || 'Action failed', 'error');
else alert(data.error || 'Action failed');
}
})
.catch(function() {
if (btn) { btn.disabled = false; btn.textContent = origText; }
if (typeof showToast === 'function') showToast('Request failed', 'error');
else alert('Request failed');
});
}
document.querySelectorAll('.plugin-theme-action-btn').forEach(function(btn) {
btn.addEventListener('click', function() {
var websiteId = parseInt(btn.getAttribute('data-website-id'), 10);
var type = btn.getAttribute('data-type');
var action = btn.getAttribute('data-action');
var plugin = btn.getAttribute('data-plugin') || '';
var theme = btn.getAttribute('data-theme') || '';
doPluginThemeAction(websiteId, type, action, plugin, theme, btn);
});
});
var extraWorkForm = document.getElementById('extra-work-form');
if (extraWorkForm) {
extraWorkForm.addEventListener('submit', function(e) {
e.preventDefault();
var btn = document.getElementById('extra-work-submit');
var origText = btn ? btn.textContent : '';
if (btn) { btn.disabled = true; btn.textContent = 'Saving…'; }
var payload = {
website_id: parseInt(extraWorkForm.querySelector('input[name="website_id"]').value, 10),
work_date: extraWorkForm.querySelector('#extra_work_date').value,
description: extraWorkForm.querySelector('#extra_work_description').value.trim(),
time_spent: parseInt(extraWorkForm.querySelector('#extra_work_time').value, 10) || 0
};
if (!payload.description || payload.time_spent < 1) {
if (btn) { btn.disabled = false; btn.textContent = origText; }
if (typeof showToast === 'function') showToast('Please enter description and time (at least 1 minute).', 'error');
else alert('Please enter description and time (at least 1 minute).');
return;
}
fetch('api/extra-work.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
})
.then(function(r) { return r.json(); })
.then(function(data) {
if (btn) { btn.disabled = false; btn.textContent = origText; }
if (data.success) {
if (typeof showToast === 'function') showToast(data.message || 'Entry added');
else alert(data.message || 'Entry added');
window.location.reload();
} else {
if (typeof showToast === 'function') showToast(data.message || 'Failed to add entry', 'error');
else alert(data.message || 'Failed to add entry');
}
})
.catch(function() {
if (btn) { btn.disabled = false; btn.textContent = origText; }
if (typeof showToast === 'function') showToast('Request failed', 'error');
else alert('Request failed');
});
});
}
})();
</script>
<?php require_once __DIR__ . '/includes/footer.php'; ?>