| Server IP : 213.255.246.8 / Your IP : 216.73.217.138 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 Detail Page with Tabbed Updates View
*/
require_once __DIR__ . '/includes/functions.php';
// Get website ID
$websiteId = isset($_GET['id']) ? (int)$_GET['id'] : 0;
if (!$websiteId) {
redirect('websites.php');
}
// Get current tab
$currentTab = isset($_GET['tab']) ? $_GET['tab'] : 'plugins';
$validTabs = ['plugins', 'themes', 'core', 'extra', 'users', 'backups'];
if (!in_array($currentTab, $validTabs)) {
$currentTab = 'plugins';
}
// Update status tab (for the "Updates available" card)
$updateStatusTab = isset($_GET['update_tab']) && in_array($_GET['update_tab'], ['core', 'plugins', 'themes'], true)
? $_GET['update_tab']
: 'core';
// Snapshot tab (for the "Installed on this site" card)
$snapshotTab = isset($_GET['snapshot_tab']) && in_array($_GET['snapshot_tab'], ['core', 'plugins', 'themes', 'users'], true)
? $_GET['snapshot_tab']
: 'core';
// Main tab: which card is visible (Updates Available | Installed on this site | Website Activity)
$mainTab = isset($_GET['main_tab']) && in_array($_GET['main_tab'], ['updates', 'installed', 'activity'], true)
? $_GET['main_tab']
: 'updates';
// Get website details
try {
$db = getDb();
$website = $db->fetchOne(
"SELECT w.*, c.client_name, c.client_id
FROM st_websites w
LEFT JOIN st_clients c ON w.client_id = c.client_id
WHERE w.website_id = ?",
[$websiteId]
);
if (!$website) {
redirect('websites.php');
}
// Get update counts
$pluginCount = $db->fetchOne("SELECT COUNT(*) as count FROM st_activity WHERE website_id = ? AND object_type = 'Plugins'", [$websiteId])['count'];
$themeCount = $db->fetchOne("SELECT COUNT(*) as count FROM st_activity WHERE website_id = ? AND object_type = 'Themes'", [$websiteId])['count'];
$coreCount = $db->fetchOne("SELECT COUNT(*) as count FROM st_activity WHERE website_id = ? AND object_type = 'Core'", [$websiteId])['count'];
$extraCount = $db->fetchOne("SELECT COUNT(*) as count FROM st_extra_work WHERE website_id = ?", [$websiteId])['count'];
$userCount = $db->fetchOne("SELECT COUNT(*) as count FROM st_activity WHERE website_id = ? AND object_type = 'Users'", [$websiteId])['count'];
$backupCount = $db->fetchOne("SELECT COUNT(*) as count FROM st_backups WHERE website_id = ?", [$websiteId])['count'];
// Uptime chart data (last 7 days)
$uptimeChartData = getUptimeChartData($websiteId, 7);
// Update status (what needs updating - from plugin report)
$updateStatus = null;
$updateStatusPlugins = [];
$updateStatusThemes = [];
try {
$updateStatus = $db->fetchOne("SELECT * FROM st_update_status WHERE website_id = ?", [$websiteId]);
if ($updateStatus) {
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 ($p) { return !empty($p['needs_update']); }) : [];
}
}
} catch (Exception $e) {
// Table may not exist
}
// Site snapshot (installed plugins, themes, core, users from plugin)
$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) {
// Table may not exist
}
// Get updates based on current tab
switch ($currentTab) {
case 'themes':
$updates = $db->fetchAll(
"SELECT activity_id, website_id, histid, action, object_name AS theme_name, object_subtype AS theme_version, user_id, username, user_caps, hist_ip, hist_time FROM st_activity WHERE website_id = ? AND object_type = 'Themes' ORDER BY hist_time DESC",
[$websiteId]
);
break;
case 'core':
$updates = $db->fetchAll(
"SELECT activity_id, website_id, histid, action, object_subtype AS version, user_id, username, user_caps, hist_ip, hist_time FROM st_activity WHERE website_id = ? AND object_type = 'Core' ORDER BY hist_time DESC",
[$websiteId]
);
break;
case 'extra':
$updates = $db->fetchAll(
"SELECT * FROM st_extra_work WHERE website_id = ? ORDER BY work_date DESC",
[$websiteId]
);
break;
case 'users':
$updates = $db->fetchAll(
"SELECT username AS performed_by_username, user_caps, object_name AS target_username, hist_time, action, action_location, hist_ip FROM st_activity WHERE website_id = ? AND object_type = 'Users' ORDER BY hist_time DESC",
[$websiteId]
);
break;
case 'backups':
$updates = $db->fetchAll(
"SELECT destination, filename, status, backup_trigger, modified FROM st_backups WHERE website_id = ? ORDER BY modified DESC",
[$websiteId]
);
break;
default: // plugins
$updates = $db->fetchAll(
"SELECT activity_id, website_id, histid, action, object_name AS plugin_name, object_subtype AS plugin_version, user_id, username, user_caps, hist_ip, hist_time FROM st_activity WHERE website_id = ? AND object_type = 'Plugins' ORDER BY hist_time DESC",
[$websiteId]
);
}
// Get clients for reassignment
$clients = getClients();
} catch (Exception $e) {
$website = null;
$updates = [];
$clients = [];
$pluginCount = $themeCount = $coreCount = $extraCount = $userCount = $backupCount = 0;
$uptimeChartData = ['labels' => [], 'uptime_pct' => [], 'response_time_ms' => []];
$updateStatus = null;
$updateStatusPlugins = [];
$updateStatusThemes = [];
$siteSnapshot = null;
$snapshotPlugins = [];
$snapshotThemes = [];
$snapshotUsers = [];
}
$pageTitle = $website ? $website['website_url'] : 'Website Details';
require_once __DIR__ . '/includes/header.php';
// Handle success message
$successMessage = isset($_GET['success']) ? $_GET['success'] : null;
?>
<!-- Breadcrumb -->
<div class="flex items-center gap-2 text-sm text-muted-foreground mb-6">
<a href="websites.php" class="hover:text-foreground" data-testid="link-back-websites">Websites</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_url']) ?></span>
</div>
<?php if ($successMessage): ?>
<div class="mb-6 p-4 rounded-md bg-green-500/10 border border-green-500/20 text-green-600 dark:text-green-400" data-testid="alert-success">
<?= sanitize($successMessage) ?>
</div>
<?php endif; ?>
<?php
$newMaintenanceSecret = isset($_GET['new_maintenance_secret']) ? trim((string) $_GET['new_maintenance_secret']) : '';
if ($newMaintenanceSecret !== ''):
?>
<div class="mb-6 p-4 rounded-md bg-amber-500/10 border border-amber-500/20" data-testid="alert-maintenance-secret">
<p class="text-sm font-medium text-amber-800 dark:text-amber-200 mb-2">Connection secret — copy this into the WordPress site</p>
<p class="text-xs text-muted-foreground mb-2">Paste it in the site under <strong>Settings → Stapolin Maintenance</strong>. This value is not shown again.</p>
<div class="flex items-center gap-2">
<input type="text"
id="new_maintenance_secret_value"
value="<?= sanitize($newMaintenanceSecret) ?>"
readonly
class="flex-1 px-3 py-2 rounded-md text-sm font-mono bg-muted">
<button type="button"
onclick="navigator.clipboard.writeText(document.getElementById('new_maintenance_secret_value').value).then(function(){ this.textContent='Copied!'; }.bind(this))"
class="btn-secondary px-3 py-2 rounded-md text-sm font-medium">
Copy
</button>
</div>
</div>
<script>if (window.history && window.history.replaceState) { var u = new URL(window.location.href); u.searchParams.delete('new_maintenance_secret'); window.history.replaceState({}, '', u.toString()); }</script>
<?php endif; ?>
<!-- Website Info Card -->
<div class="card rounded-lg p-6 mb-6">
<div class="flex items-start justify-between gap-4">
<div class="flex items-center gap-4">
<div class="w-16 h-16 rounded-lg bg-blue-500/10 flex items-center justify-center flex-shrink-0">
<svg class="w-8 h-8 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9"/>
</svg>
</div>
<div>
<h2 class="text-2xl font-semibold mb-1" data-testid="text-website-url"><?= sanitize($website['website_url']) ?></h2>
<div class="flex items-center gap-4 text-sm">
<?php if (!empty($website['website_name'])): ?>
<span class="text-muted-foreground"><?= sanitize($website['website_name']) ?></span>
<?php endif; ?>
<?php if ($website['client_id']): ?>
<a href="client-detail.php?id=<?= $website['client_id'] ?>"
class="flex items-center gap-1 text-primary hover:underline"
data-testid="link-client">
<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="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/>
</svg>
<?= sanitize($website['client_name']) ?>
</a>
<?php else: ?>
<span class="text-muted-foreground">No client assigned</span>
<?php endif; ?>
</div>
</div>
</div>
<div class="flex items-center gap-2">
<a href="<?= sanitize($website['website_url']) ?>"
target="_blank"
class="btn-secondary px-3 py-2 rounded-md text-sm font-medium flex items-center gap-2"
data-testid="button-visit-website">
<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="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"/>
</svg>
Visit
</a>
<button onclick="document.getElementById('editWebsiteModal').classList.remove('hidden')"
class="btn-secondary px-3 py-2 rounded-md text-sm font-medium flex items-center gap-2"
data-testid="button-edit-website">
<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 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/>
</svg>
Edit
</button>
</div>
</div>
</div>
<!-- Uptime & response time -->
<?php $hasUptimeData = !empty($uptimeChartData['labels']) && (array_filter($uptimeChartData['uptime_pct']) !== [] || array_filter($uptimeChartData['response_time_ms']) !== []); ?>
<div class="card rounded-lg p-6 mb-6">
<h3 class="text-lg font-semibold mb-4 flex items-center gap-2">
<svg class="w-5 h-5 text-emerald-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/>
</svg>
Uptime & response time
</h3>
<?php if (!$hasUptimeData): ?>
<p class="text-muted-foreground text-sm">No uptime data yet for this site. Data is collected when the uptime cron runs (e.g. <code class="text-xs bg-muted px-1 rounded">api/cron-uptime.php</code>).</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 % (last 7 days)</p>
<div class="h-64">
<canvas id="uptimeChart" aria-label="Uptime percentage by day"></canvas>
</div>
</div>
<div>
<p class="text-sm text-muted-foreground mb-2">Response time (ms, last 7 days)</p>
<div class="h-64">
<canvas id="responseTimeChart" aria-label="Average response time by day"></canvas>
</div>
</div>
</div>
<script>
window.__uptimeChartData = <?= json_encode($uptimeChartData) ?>;
</script>
<?php endif; ?>
</div>
<?php
$baseParams = 'id=' . $websiteId . '&tab=' . urlencode($currentTab) . '&update_tab=' . urlencode($updateStatusTab) . '&snapshot_tab=' . urlencode($snapshotTab);
$updatesParams = $baseParams . '&main_tab=updates';
$installedParams = $baseParams . '&main_tab=installed';
$activityParams = $baseParams . '&main_tab=activity';
?>
<!-- Single tabbed card: Updates Available | Installed on this site | Website Activity -->
<div class="card rounded-lg overflow-hidden mb-6">
<div class="border-b">
<nav class="flex -mb-px" aria-label="Main section tabs">
<a href="?<?= $updatesParams ?>"
class="px-6 py-4 text-sm font-medium border-b-2 <?= $mainTab === 'updates' ? 'border-primary text-primary' : 'border-transparent text-muted-foreground hover:text-foreground hover:border-border' ?>">
Updates available
</a>
<a href="?<?= $installedParams ?>"
class="px-6 py-4 text-sm font-medium border-b-2 <?= $mainTab === 'installed' ? 'border-primary text-primary' : 'border-transparent text-muted-foreground hover:text-foreground hover:border-border' ?>">
Installed on this site
</a>
<a href="?<?= $activityParams ?>"
class="px-6 py-4 text-sm font-medium border-b-2 <?= $mainTab === 'activity' ? 'border-primary text-primary' : 'border-transparent text-muted-foreground hover:text-foreground hover:border-border' ?>">
Website Activity
</a>
</nav>
</div>
<?php if ($mainTab === 'updates'): ?>
<!-- Updates available content -->
<div class="px-6 py-4 border-b flex items-center justify-between gap-4 flex-wrap bg-muted/20">
<?php if ($updateStatus && !empty($updateStatus['checked_at'])): ?>
<span class="text-sm text-muted-foreground">Last checked: <?= formatDateTime($updateStatus['checked_at']) ?></span>
<?php endif; ?>
</div>
<div class="border-b">
<nav class="flex -mb-px" aria-label="Update status tabs">
<a href="?<?= $updatesParams ?>&update_tab=core"
class="px-6 py-4 text-sm font-medium border-b-2 <?= $updateStatusTab === 'core' ? 'border-primary text-primary' : 'border-transparent text-muted-foreground hover:text-foreground hover:border-border' ?>">
<span class="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
<?php if ($updateStatus && !empty($updateStatus['core_needs_update'])): ?>
<span class="badge badge-info">1</span>
<?php else: ?>
<span class="badge badge-secondary">0</span>
<?php endif; ?>
</span>
</a>
<a href="?<?= $updatesParams ?>&update_tab=plugins"
class="px-6 py-4 text-sm font-medium border-b-2 <?= $updateStatusTab === 'plugins' ? 'border-primary text-primary' : 'border-transparent text-muted-foreground hover:text-foreground hover:border-border' ?>">
<span class="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>
</span>
</a>
<a href="?<?= $updatesParams ?>&update_tab=themes"
class="px-6 py-4 text-sm font-medium border-b-2 <?= $updateStatusTab === 'themes' ? 'border-primary text-primary' : 'border-transparent text-muted-foreground hover:text-foreground hover:border-border' ?>">
<span class="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>
</span>
</a>
</nav>
</div>
<div class="overflow-x-auto">
<?php if (!$updateStatus): ?>
<div class="p-12 text-center">
<p class="text-muted-foreground">No update status reported for this site yet. Data is sent by the Stapolin Activity plugin when it runs its update check.</p>
</div>
<?php elseif ($updateStatusTab === 'core'): ?>
<div class="p-6">
<?php if (!empty($updateStatus['core_needs_update'])): ?>
<table class="w-full text-sm">
<thead class="bg-muted/50">
<tr>
<th class="px-4 py-2 text-left text-xs font-medium text-muted-foreground uppercase">Current</th>
<th class="px-4 py-2 text-left text-xs font-medium text-muted-foreground uppercase">Latest</th>
</tr>
</thead>
<tbody>
<tr>
<td class="px-4 py-3 font-mono"><?= sanitize($updateStatus['core_current'] ?? '—') ?></td>
<td class="px-4 py-3 font-mono"><?= sanitize($updateStatus['core_latest'] ?? '—') ?></td>
</tr>
</tbody>
</table>
<p class="mt-3 text-sm text-amber-600 dark:text-amber-400">WordPress core has an update available.</p>
<?php else: ?>
<p class="text-muted-foreground">WordPress core is up to date.</p>
<?php if (!empty($updateStatus['core_current'])): ?>
<p class="text-sm font-mono mt-1">Version: <?= sanitize($updateStatus['core_current']) ?></p>
<?php endif; ?>
<?php endif; ?>
</div>
<?php elseif ($updateStatusTab === 'plugins'): ?>
<div class="p-6">
<?php if (empty($updateStatusPlugins)): ?>
<p class="text-muted-foreground">All plugins are up to date.</p>
<?php else: ?>
<table class="w-full text-sm">
<thead class="bg-muted/50">
<tr>
<th class="px-4 py-2 text-left text-xs font-medium text-muted-foreground uppercase">Plugin</th>
<th class="px-4 py-2 text-left text-xs font-medium text-muted-foreground uppercase">Current</th>
<th class="px-4 py-2 text-left text-xs font-medium text-muted-foreground uppercase">Latest</th>
</tr>
</thead>
<tbody class="divide-y">
<?php foreach ($updateStatusPlugins as $p): ?>
<tr>
<td class="px-4 py-3 font-medium"><?= sanitize($p['name'] ?? '—') ?></td>
<td class="px-4 py-3 font-mono"><?= sanitize($p['current'] ?? '—') ?></td>
<td class="px-4 py-3 font-mono"><?= sanitize($p['latest'] ?? '—') ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>
<?php else: ?>
<div class="p-6">
<?php if (empty($updateStatusThemes)): ?>
<p class="text-muted-foreground">All themes are up to date.</p>
<?php else: ?>
<table class="w-full text-sm">
<thead class="bg-muted/50">
<tr>
<th class="px-4 py-2 text-left text-xs font-medium text-muted-foreground uppercase">Theme</th>
<th class="px-4 py-2 text-left text-xs font-medium text-muted-foreground uppercase">Current</th>
<th class="px-4 py-2 text-left text-xs font-medium text-muted-foreground uppercase">Latest</th>
</tr>
</thead>
<tbody class="divide-y">
<?php foreach ($updateStatusThemes as $t): ?>
<tr>
<td class="px-4 py-3 font-medium"><?= sanitize($t['name'] ?? '—') ?></td>
<td class="px-4 py-3 font-mono"><?= sanitize($t['current'] ?? '—') ?></td>
<td class="px-4 py-3 font-mono"><?= sanitize($t['latest'] ?? '—') ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>
<?php endif; ?>
</div>
<?php elseif ($mainTab === 'installed'): ?>
<!-- Installed on this site content -->
<div class="px-6 py-4 border-b flex items-center justify-between gap-4 flex-wrap bg-muted/20">
<?php if ($siteSnapshot && !empty($siteSnapshot['checked_at'])): ?>
<span class="text-sm text-muted-foreground">Last synced: <?= formatDateTime($siteSnapshot['checked_at']) ?></span>
<?php endif; ?>
</div>
<div class="border-b">
<nav class="flex -mb-px" aria-label="Installed snapshot tabs">
<a href="?<?= $installedParams ?>&snapshot_tab=core" class="px-6 py-4 text-sm font-medium border-b-2 <?= $snapshotTab === 'core' ? 'border-primary text-primary' : 'border-transparent text-muted-foreground hover:text-foreground hover:border-border' ?>">
<span class="flex items-center gap-2">Core</span>
</a>
<a href="?<?= $installedParams ?>&snapshot_tab=plugins" class="px-6 py-4 text-sm font-medium border-b-2 <?= $snapshotTab === 'plugins' ? 'border-primary text-primary' : 'border-transparent text-muted-foreground hover:text-foreground hover:border-border' ?>">
<span class="flex items-center gap-2">Plugins <span class="badge badge-success"><?= count($snapshotPlugins) ?></span></span>
</a>
<a href="?<?= $installedParams ?>&snapshot_tab=themes" class="px-6 py-4 text-sm font-medium border-b-2 <?= $snapshotTab === 'themes' ? 'border-primary text-primary' : 'border-transparent text-muted-foreground hover:text-foreground hover:border-border' ?>">
<span class="flex items-center gap-2">Themes <span class="badge badge-warning"><?= count($snapshotThemes) ?></span></span>
</a>
<a href="?<?= $installedParams ?>&snapshot_tab=users" class="px-6 py-4 text-sm font-medium border-b-2 <?= $snapshotTab === 'users' ? 'border-primary text-primary' : 'border-transparent text-muted-foreground hover:text-foreground hover:border-border' ?>">
<span class="flex items-center gap-2">Users <span class="badge badge-info"><?= count($snapshotUsers) ?></span></span>
</a>
</nav>
</div>
<div class="overflow-x-auto">
<?php if (!$siteSnapshot): ?>
<div class="p-12 text-center">
<p class="text-muted-foreground">No snapshot for this site yet. The Stapolin Activity plugin sends installed plugins, themes, core version and users every hour.</p>
</div>
<?php elseif ($snapshotTab === 'core'): ?>
<div class="p-6">
<p class="text-muted-foreground">WordPress version: <span class="font-mono font-medium"><?= sanitize($siteSnapshot['core_version'] ?? '—') ?></span></p>
</div>
<?php elseif ($snapshotTab === 'plugins'): ?>
<div class="p-6">
<?php if (empty($snapshotPlugins)): ?>
<p class="text-muted-foreground">No plugins reported in snapshot.</p>
<?php else: ?>
<table class="w-full text-sm">
<thead class="bg-muted/50">
<tr>
<th class="px-4 py-2 text-left text-xs font-medium text-muted-foreground uppercase">Plugin</th>
<th class="px-4 py-2 text-left text-xs font-medium text-muted-foreground uppercase">Version</th>
</tr>
</thead>
<tbody class="divide-y">
<?php foreach ($snapshotPlugins as $p): ?>
<tr>
<td class="px-4 py-3 font-medium"><?= sanitize($p['name'] ?? '—') ?></td>
<td class="px-4 py-3 font-mono"><?= sanitize($p['version'] ?? '—') ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>
<?php elseif ($snapshotTab === 'themes'): ?>
<div class="p-6">
<?php if (empty($snapshotThemes)): ?>
<p class="text-muted-foreground">No themes reported in snapshot.</p>
<?php else: ?>
<table class="w-full text-sm">
<thead class="bg-muted/50">
<tr>
<th class="px-4 py-2 text-left text-xs font-medium text-muted-foreground uppercase">Theme</th>
<th class="px-4 py-2 text-left text-xs font-medium text-muted-foreground uppercase">Version</th>
</tr>
</thead>
<tbody class="divide-y">
<?php foreach ($snapshotThemes as $t): ?>
<tr>
<td class="px-4 py-3 font-medium"><?= sanitize($t['name'] ?? '—') ?></td>
<td class="px-4 py-3 font-mono"><?= sanitize($t['version'] ?? '—') ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>
<?php else: ?>
<div class="p-6">
<?php if (empty($snapshotUsers)): ?>
<p class="text-muted-foreground">No users reported in snapshot.</p>
<?php else: ?>
<table class="w-full text-sm">
<thead class="bg-muted/50">
<tr>
<th class="px-4 py-2 text-left text-xs font-medium text-muted-foreground uppercase">Username</th>
<th class="px-4 py-2 text-left text-xs font-medium text-muted-foreground uppercase">Email</th>
<th class="px-4 py-2 text-left text-xs font-medium text-muted-foreground uppercase">Roles</th>
</tr>
</thead>
<tbody class="divide-y">
<?php foreach ($snapshotUsers as $u): ?>
<tr>
<td class="px-4 py-3 font-medium"><?= sanitize($u['username'] ?? '—') ?></td>
<td class="px-4 py-3"><?= sanitize($u['email'] ?? '—') ?></td>
<td class="px-4 py-3"><?= sanitize(is_array($u['roles'] ?? null) ? implode(', ', $u['roles']) : '—') ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>
<?php endif; ?>
</div>
<?php elseif ($mainTab === 'activity'): ?>
<!-- Website Activity content -->
<div class="border-b">
<nav class="flex -mb-px" aria-label="Tabs">
<a href="?<?= $activityParams ?>&tab=plugins"
class="px-6 py-4 text-sm font-medium border-b-2 <?= $currentTab === 'plugins' ? 'border-primary text-primary' : 'border-transparent text-muted-foreground hover:text-foreground hover:border-border' ?>"
data-testid="tab-plugins">
<span class="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 badge-success"><?= $pluginCount ?></span>
</span>
</a>
<a href="?<?= $activityParams ?>&tab=themes"
class="px-6 py-4 text-sm font-medium border-b-2 <?= $currentTab === 'themes' ? 'border-primary text-primary' : 'border-transparent text-muted-foreground hover:text-foreground hover:border-border' ?>"
data-testid="tab-themes">
<span class="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 badge-warning"><?= $themeCount ?></span>
</span>
</a>
<a href="?<?= $activityParams ?>&tab=core"
class="px-6 py-4 text-sm font-medium border-b-2 <?= $currentTab === 'core' ? 'border-primary text-primary' : 'border-transparent text-muted-foreground hover:text-foreground hover:border-border' ?>"
data-testid="tab-core">
<span class="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 Updates
<span class="badge badge-info"><?= $coreCount ?></span>
</span>
</a>
<a href="?<?= $activityParams ?>&tab=extra"
class="px-6 py-4 text-sm font-medium border-b-2 <?= $currentTab === 'extra' ? 'border-primary text-primary' : 'border-transparent text-muted-foreground hover:text-foreground hover:border-border' ?>"
data-testid="tab-extra">
<span class="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="M12 6v6m0 0v6m0-6h6m-6 0H6"/>
</svg>
Extra Work
<span class="badge badge-primary"><?= $extraCount ?></span>
</span>
</a>
<a href="?<?= $activityParams ?>&tab=users"
class="px-6 py-4 text-sm font-medium border-b-2 <?= $currentTab === 'users' ? 'border-primary text-primary' : 'border-transparent text-muted-foreground hover:text-foreground hover:border-border' ?>"
data-testid="tab-users">
<span class="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="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"/>
</svg>
User Activity
<span class="badge badge-secondary"><?= $userCount ?></span>
</span>
</a>
<a href="?<?= $activityParams ?>&tab=backups"
class="px-6 py-4 text-sm font-medium border-b-2 <?= $currentTab === 'backups' ? 'border-primary text-primary' : 'border-transparent text-muted-foreground hover:text-foreground hover:border-border' ?>"
data-testid="tab-backups">
<span class="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 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4"/>
</svg>
Backups
<span class="badge badge-info"><?= $backupCount ?></span>
</span>
</a>
</nav>
</div>
<!-- Tab Content -->
<div class="overflow-x-auto">
<?php if (empty($updates)): ?>
<div class="p-12 text-center">
<svg class="w-16 h-16 mx-auto text-muted-foreground/50 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4"/>
</svg>
<h4 class="text-lg font-medium mb-2">No <?= $currentTab === 'users' ? 'user activity' : ($currentTab === 'backups' ? 'backups' : $currentTab . ' updates') ?> found</h4>
<p class="text-muted-foreground"><?= $currentTab === 'users' ? 'User login activity will appear here once tracked' : ($currentTab === 'backups' ? 'Backup records will appear here once tracked' : 'Updates will appear here once they are tracked') ?></p>
</div>
<?php else: ?>
<table class="w-full">
<thead class="bg-muted/50">
<tr>
<?php if ($currentTab === 'plugins'): ?>
<th class="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">Plugin Name</th>
<th class="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">Version</th>
<th class="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">Action</th>
<th class="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">Date</th>
<?php elseif ($currentTab === 'themes'): ?>
<th class="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">Theme Name</th>
<th class="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">Version</th>
<th class="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">Action</th>
<th class="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">Date</th>
<?php elseif ($currentTab === 'core'): ?>
<th class="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">Version</th>
<th class="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">Action</th>
<th class="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">Date</th>
<?php elseif ($currentTab === 'users'): ?>
<th class="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">Performed By</th>
<th class="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">User Role</th>
<th class="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">Target User</th>
<th class="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">Action</th>
<th class="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">Location</th>
<th class="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">IP Address</th>
<th class="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">Date/Time</th>
<?php elseif ($currentTab === 'backups'): ?>
<th class="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">Destination</th>
<th class="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">Filename</th>
<th class="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">Status</th>
<th class="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">Trigger</th>
<th class="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">Modified</th>
<?php else: ?>
<th class="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">Description</th>
<th class="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">Duration</th>
<th class="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">Date</th>
<?php endif; ?>
</tr>
</thead>
<tbody class="divide-y">
<?php foreach ($updates as $update): ?>
<tr class="table-row">
<?php if ($currentTab === 'plugins'): ?>
<td class="px-6 py-4 font-medium"><?= sanitize($update['plugin_name']) ?></td>
<td class="px-6 py-4 font-mono text-sm"><?= sanitize($update['plugin_version'] ?? '-') ?></td>
<td class="px-6 py-4">
<span class="badge <?= $update['action'] === 'update' ? 'badge-success' : ($update['action'] === 'install' ? 'badge-info' : 'badge-warning') ?>">
<?= sanitize(ucfirst($update['action'] ?? 'update')) ?>
</span>
</td>
<td class="px-6 py-4 text-sm text-muted-foreground"><?= formatDateTime($update['hist_time']) ?></td>
<?php elseif ($currentTab === 'themes'): ?>
<td class="px-6 py-4 font-medium"><?= sanitize($update['theme_name']) ?></td>
<td class="px-6 py-4 font-mono text-sm"><?= sanitize($update['theme_version'] ?? '-') ?></td>
<td class="px-6 py-4">
<span class="badge <?= $update['action'] === 'update' ? 'badge-success' : ($update['action'] === 'install' ? 'badge-info' : 'badge-warning') ?>">
<?= sanitize(ucfirst($update['action'] ?? 'update')) ?>
</span>
</td>
<td class="px-6 py-4 text-sm text-muted-foreground"><?= formatDateTime($update['hist_time']) ?></td>
<?php elseif ($currentTab === 'core'): ?>
<td class="px-6 py-4 font-mono"><?= sanitize($update['version']) ?></td>
<td class="px-6 py-4">
<span class="badge badge-info"><?= sanitize(ucfirst($update['action'] ?? 'update')) ?></span>
</td>
<td class="px-6 py-4 text-sm text-muted-foreground"><?= formatDateTime($update['hist_time']) ?></td>
<?php elseif ($currentTab === 'users'): ?>
<td class="px-6 py-4 font-medium"><?= sanitize($update['performed_by_username'] ?? '-') ?></td>
<td class="px-6 py-4 text-sm"><?= sanitize($update['user_caps'] ?? '-') ?></td>
<td class="px-6 py-4"><?= sanitize($update['target_username'] ?? '-') ?></td>
<td class="px-6 py-4">
<?php
$action = $update['action'] ?? '';
$actionLabel = str_replace('_', ' ', ucfirst($action));
$badgeClass = 'badge-secondary';
if ($action === 'logged_in') $badgeClass = 'badge-success';
elseif ($action === 'logged_out') $badgeClass = 'badge-warning';
elseif ($action === 'registered') $badgeClass = 'badge-info';
elseif ($action === 'updated') $badgeClass = 'badge-primary';
?>
<span class="badge <?= $badgeClass ?>"><?= sanitize($actionLabel) ?></span>
</td>
<td class="px-6 py-4 text-sm"><?= sanitize($update['action_location'] ?? '-') ?></td>
<td class="px-6 py-4 text-sm font-mono"><?= sanitize($update['hist_ip'] ?? '-') ?></td>
<td class="px-6 py-4 text-sm text-muted-foreground"><?= formatDateTime($update['hist_time']) ?></td>
<?php elseif ($currentTab === 'backups'): ?>
<td class="px-6 py-4">
<span class="badge badge-secondary"><?= sanitize(strtoupper($update['destination'] ?? '-')) ?></span>
</td>
<td class="px-6 py-4 font-mono text-sm"><?= sanitize($update['filename'] ?? '-') ?></td>
<td class="px-6 py-4">
<?php
$status = strtolower($update['status'] ?? '');
$statusLabel = ucfirst($status);
$statusClass = 'badge-secondary';
if ($status === 'completed' || $status === 'success') $statusClass = 'badge-success';
elseif ($status === 'failed' || $status === 'error') $statusClass = 'badge-destructive';
elseif ($status === 'pending' || $status === 'running') $statusClass = 'badge-warning';
?>
<span class="badge <?= $statusClass ?>"><?= sanitize($statusLabel) ?></span>
</td>
<td class="px-6 py-4 text-sm"><?= sanitize(str_replace('_', ' ', ucfirst($update['backup_trigger'] ?? '-'))) ?></td>
<td class="px-6 py-4 text-sm text-muted-foreground"><?= formatDateTime($update['modified']) ?></td>
<?php else: ?>
<td class="px-6 py-4"><?= sanitize($update['description']) ?></td>
<td class="px-6 py-4"><?= formatDuration($update['duration_minutes'] ?? 0) ?></td>
<td class="px-6 py-4 text-sm text-muted-foreground"><?= formatDate($update['work_date']) ?></td>
<?php endif; ?>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>
<?php endif; ?>
</div>
<!-- Edit Website Modal -->
<div id="editWebsiteModal" class="hidden fixed inset-0 z-50 overflow-y-auto" aria-modal="true">
<div class="flex items-center justify-center min-h-screen px-4 pt-4 pb-20 text-center sm:block sm:p-0">
<div class="fixed inset-0 bg-black/50 transition-opacity" onclick="document.getElementById('editWebsiteModal').classList.add('hidden')"></div>
<div class="inline-block align-bottom card rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full">
<form action="api/websites.php" method="POST">
<input type="hidden" name="action" value="update">
<input type="hidden" name="website_id" value="<?= $website['website_id'] ?>">
<input type="hidden" name="redirect" value="website-detail.php?id=<?= $website['website_id'] ?>">
<div class="px-6 py-4 border-b">
<h3 class="text-lg font-semibold">Edit Website</h3>
</div>
<div class="px-6 py-4 space-y-4">
<div>
<label for="edit_website_url" class="block text-sm font-medium mb-1">Website URL <span class="text-destructive">*</span></label>
<input type="url"
id="edit_website_url"
name="website_url"
required
value="<?= sanitize($website['website_url']) ?>"
class="w-full px-3 py-2 rounded-md text-sm"
data-testid="input-edit-website-url">
</div>
<div>
<label for="edit_website_name" class="block text-sm font-medium mb-1">Website Name</label>
<input type="text"
id="edit_website_name"
name="website_name"
value="<?= sanitize($website['website_name'] ?? '') ?>"
class="w-full px-3 py-2 rounded-md text-sm"
data-testid="input-edit-website-name">
</div>
<div>
<label for="edit_client_id" class="block text-sm font-medium mb-1">Client</label>
<select id="edit_client_id"
name="client_id"
class="w-full px-3 py-2 rounded-md text-sm"
data-testid="select-edit-client">
<option value="">No client assigned</option>
<?php foreach ($clients as $client): ?>
<option value="<?= $client['client_id'] ?>" <?= $client['client_id'] == $website['client_id'] ? 'selected' : '' ?>>
<?= sanitize($client['client_name']) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="border-t pt-4 mt-4">
<p class="text-xs text-muted-foreground mb-2">Optional: use for hosts that require an admin session for remote updates (e.g. some WP Engine setups). Create an <a href="https://wordpress.org/support/article/application-passwords/" target="_blank" rel="noopener" class="text-primary hover:underline">Application Password</a> under Users → Profile on the WordPress site.</p>
<div class="space-y-2">
<label for="edit_wp_auth_username" class="block text-sm font-medium">WordPress admin username</label>
<input type="text"
id="edit_wp_auth_username"
name="wp_auth_username"
value="<?= sanitize($website['wp_auth_username'] ?? '') ?>"
class="w-full px-3 py-2 rounded-md text-sm"
placeholder="admin"
autocomplete="off">
<label for="edit_wp_auth_app_password" class="block text-sm font-medium">Application password</label>
<input type="password"
id="edit_wp_auth_app_password"
name="wp_auth_app_password"
value=""
class="w-full px-3 py-2 rounded-md text-sm"
placeholder="Leave blank to keep existing"
autocomplete="new-password">
</div>
</div>
<div class="border-t pt-4 mt-4">
<p class="text-xs text-muted-foreground mb-2"><strong>Stapolin Maintenance</strong>: no Authorization header. Install the plugin on the site and paste the connection secret in Settings → Stapolin Maintenance. Use this for WP Engine or when Application Password fails.</p>
<p class="text-sm mb-2">Connection secret: <?= !empty($website['maintenance_secret']) ? '<span class="text-green-600 dark:text-green-400">Set</span>' : '<span class="text-muted-foreground">Not set</span>' ?></p>
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" name="regenerate_maintenance_secret" value="1" id="regenerate_maintenance_secret" class="rounded">
<span class="text-sm">Regenerate connection secret (new secret shown after save; paste into the site)</span>
</label>
</div>
<div class="border-t pt-4 mt-4">
<p class="text-xs text-muted-foreground mb-2"><strong>WP Manager Companion</strong>: paste the Pairing Token from the plugin settings on the WordPress site. The dashboard will complete the pairing and use WP Manager for updates on this site.</p>
<p class="text-sm mb-2">
WP Manager status:
<?php if (!empty($website['wp_manager_client_id'] ?? '')): ?>
<span class="text-green-600 dark:text-green-400">Paired</span>
<?php else: ?>
<span class="text-muted-foreground">Not paired</span>
<?php endif; ?>
</p>
<div class="space-y-2">
<label for="edit_wp_manager_pairing_token" class="block text-sm font-medium">Pairing Token</label>
<input type="text"
id="edit_wp_manager_pairing_token"
name="wp_manager_pairing_token"
value=""
class="w-full px-3 py-2 rounded-md text-sm"
placeholder="Paste token from WP Manager Companion settings"
autocomplete="off">
<?php if (!empty($website['wp_manager_client_id'] ?? '')): ?>
<label class="flex items-center gap-2 cursor-pointer mt-2">
<input type="checkbox" name="clear_wp_manager" value="1" class="rounded">
<span class="text-sm">Clear existing WP Manager connection (remove client credentials)</span>
</label>
<?php endif; ?>
</div>
</div>
</div>
<div class="px-6 py-4 border-t flex items-center justify-end gap-3">
<button type="button"
onclick="document.getElementById('editWebsiteModal').classList.add('hidden')"
class="btn-secondary px-4 py-2 rounded-md text-sm font-medium">
Cancel
</button>
<button type="submit"
class="btn-primary px-4 py-2 rounded-md text-sm font-medium"
data-testid="button-submit-edit-website">
Save Changes
</button>
</div>
</form>
</div>
</div>
</div>
<?php if (!empty($hasUptimeData)): ?>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
<script>
(function() {
var d = window.__uptimeChartData;
if (!d || !d.labels || !d.labels.length) return;
var opts = { responsive: true, maintainAspectRatio: false };
new Chart(document.getElementById('uptimeChart'), {
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('responseTimeChart'), {
type: 'line',
data: {
labels: d.labels,
datasets: [{ label: 'Response time (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 require_once __DIR__ . '/includes/footer.php'; ?>