| Server IP : 213.255.246.8 / Your IP : 216.73.217.0 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
/**
* Updates overview: website cards with update counts (core, plugins, themes).
* Each card links to the website dashboard.
* Filterable by update status (has updates / up to date) and by client.
*/
$pageTitle = 'Updates';
require_once __DIR__ . '/includes/functions.php';
$filterUpdates = isset($_GET['filter_updates']) ? trim($_GET['filter_updates']) : '';
$filterClient = isset($_GET['filter_client']) ? trim($_GET['filter_client']) : '';
if (!in_array($filterUpdates, ['', 'all', 'has_updates', 'no_updates'], true)) {
$filterUpdates = '';
}
if ($filterUpdates === 'all') {
$filterUpdates = '';
}
$clients = getClients();
try {
$db = getDb();
$where = ['1=1'];
$params = [];
if ($filterClient !== '' && $filterClient !== 'all') {
if ($filterClient === 'none') {
$where[] = 'w.client_id IS NULL';
} else {
$where[] = 'w.client_id = ?';
$params[] = (int) $filterClient;
}
}
$hasUpdatesExpr = "(us.status_id IS NOT NULL AND (us.core_needs_update = 1 OR (us.plugins_json IS NOT NULL AND us.plugins_json LIKE '%\"needs_update\":true%') OR (us.themes_json IS NOT NULL AND us.themes_json LIKE '%\"needs_update\":true%')))";
if ($filterUpdates === 'has_updates') {
$where[] = $hasUpdatesExpr;
} elseif ($filterUpdates === 'no_updates') {
$where[] = "NOT " . $hasUpdatesExpr;
}
$sql = "SELECT w.*, c.client_name,
COALESCE(s.plugin_count, 0) as snapshot_plugin_count,
COALESCE(s.theme_count, 0) as snapshot_theme_count,
(us.status_id IS NOT NULL AND (
us.core_needs_update = 1
OR (us.plugins_json IS NOT NULL AND us.plugins_json LIKE '%\"needs_update\":true%')
OR (us.themes_json IS NOT NULL AND us.themes_json LIKE '%\"needs_update\":true%')
)) AS has_updates,
(lu.latest_status IS NOT NULL AND LOWER(lu.latest_status) = 'down') AS is_down,
us.core_needs_update,
us.plugins_json,
us.themes_json
FROM st_websites w
LEFT JOIN st_clients c ON w.client_id = c.client_id
LEFT JOIN st_site_snapshot s ON s.website_id = w.website_id
LEFT JOIN st_update_status us ON us.website_id = w.website_id
LEFT JOIN (
SELECT u1.website_id, u1.status AS latest_status
FROM st_uptime_checks u1
INNER JOIN (SELECT website_id, MAX(checked_at) AS max_checked FROM st_uptime_checks GROUP BY website_id) u2
ON u1.website_id = u2.website_id AND u1.checked_at = u2.max_checked
) lu ON lu.website_id = w.website_id
WHERE " . implode(' AND ', $where) . "
ORDER BY c.client_name, w.website_url";
$rows = $db->fetchAll($sql, $params);
$websites = [];
foreach ($rows as $r) {
$coreCount = !empty($r['core_needs_update']) ? 1 : 0;
$pluginCount = 0;
$themeCount = 0;
if (!empty($r['plugins_json'])) {
$arr = json_decode($r['plugins_json'], true);
if (is_array($arr)) {
foreach ($arr as $p) {
if (!empty($p['needs_update'])) $pluginCount++;
}
}
}
if (!empty($r['themes_json'])) {
$arr = json_decode($r['themes_json'], true);
if (is_array($arr)) {
foreach ($arr as $t) {
if (!empty($t['needs_update'])) $themeCount++;
}
}
}
$r['core_updates_count'] = $coreCount;
$r['plugin_updates_count'] = $pluginCount;
$r['theme_updates_count'] = $themeCount;
$r['plugin_count'] = $r['snapshot_plugin_count'];
$r['theme_count'] = $r['snapshot_theme_count'];
$websites[] = $r;
}
} catch (Exception $e) {
$websites = [];
}
require_once __DIR__ . '/includes/header.php';
$hasActiveFilters = $filterUpdates !== '' || ($filterClient !== '' && $filterClient !== 'all');
?>
<div class="mb-6 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<p class="text-muted-foreground">Sites with available updates. Click a card to open the website dashboard.</p>
<form method="get" action="updates.php" class="flex flex-wrap items-center gap-3">
<label class="flex items-center gap-2 text-sm">
<span class="text-muted-foreground whitespace-nowrap">Updates</span>
<select name="filter_updates" class="rounded-md border border-border bg-background px-3 py-2 text-sm" onchange="this.form.submit()">
<option value="all"<?= $filterUpdates === '' ? ' selected' : '' ?>>All sites</option>
<option value="has_updates"<?= $filterUpdates === 'has_updates' ? ' selected' : '' ?>>Has updates</option>
<option value="no_updates"<?= $filterUpdates === 'no_updates' ? ' selected' : '' ?>>Up to date</option>
</select>
</label>
<label class="flex items-center gap-2 text-sm">
<span class="text-muted-foreground whitespace-nowrap">Client</span>
<select name="filter_client" class="rounded-md border border-border bg-background px-3 py-2 text-sm min-w-[140px]" onchange="this.form.submit()">
<option value="all"<?= $filterClient === '' || $filterClient === 'all' ? ' selected' : '' ?>>All clients</option>
<option value="none"<?= $filterClient === 'none' ? ' selected' : '' ?>>No client</option>
<?php foreach ($clients as $c): ?>
<option value="<?= (int)$c['client_id'] ?>"<?= $filterClient === (string)$c['client_id'] ? ' selected' : '' ?>><?= sanitize($c['client_name'] ?? '') ?></option>
<?php endforeach; ?>
</select>
</label>
<?php if ($hasActiveFilters): ?>
<a href="updates.php" class="text-sm text-primary hover:underline">Clear filters</a>
<?php endif; ?>
</form>
</div>
<?php if (empty($websites)): ?>
<div class="card rounded-lg 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="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>
<h3 class="text-lg font-medium mb-2"><?= $hasActiveFilters ? 'No sites match the filters' : 'No websites yet' ?></h3>
<p class="text-muted-foreground">
<?php if ($hasActiveFilters): ?>
<a href="updates.php" class="text-primary hover:underline">Clear filters</a> to see all sites.
<?php else: ?>
Add websites from the <a href="websites.php" class="text-primary hover:underline">Websites</a> page to see update status here.
<?php endif; ?>
</p>
</div>
<?php else: ?>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
<?php foreach ($websites as $w): ?>
<?php
$isDown = !empty($w['is_down']);
$hasUpdates = !empty($w['has_updates']);
$totalUpdates = (int)$w['core_updates_count'] + (int)$w['plugin_updates_count'] + (int)$w['theme_updates_count'];
$hasConnection = !empty($w['maintenance_secret']) || (!empty($w['wp_auth_username']) && !empty($w['wp_auth_app_password']));
?>
<a href="website-dashboard.php?id=<?= (int)$w['website_id'] ?>" class="card rounded-lg overflow-hidden hover:border-primary/50 transition-colors block" data-testid="card-website-<?= $w['website_id'] ?>">
<div class="aspect-video bg-muted/50 flex items-center justify-center">
<?php if (!empty($w['screenshot_url'])): ?>
<img src="<?= sanitize($w['screenshot_url']) ?>" alt="" class="w-full h-full object-cover">
<?php else: ?>
<svg class="w-16 h-16 text-muted-foreground/40" 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>
<?php endif; ?>
</div>
<div class="p-4">
<div class="flex items-start justify-between gap-2 mb-2">
<h3 class="font-medium truncate flex-1" title="<?= sanitize($w['website_name'] ?: $w['website_url']) ?>"><?= sanitize($w['website_name'] ?: $w['website_url']) ?></h3>
<div class="flex items-center gap-2">
<?php if ($isDown): ?>
<span class="w-2.5 h-2.5 rounded-full bg-red-500 flex-shrink-0" title="Site down"></span>
<?php elseif ($hasUpdates): ?>
<span class="w-2.5 h-2.5 rounded-full bg-amber-500 flex-shrink-0" title="Updates available"></span>
<?php else: ?>
<span class="w-2.5 h-2.5 rounded-full bg-emerald-500 flex-shrink-0" title="Up to date"></span>
<?php endif; ?>
<button
type="button"
class="site-refresh-btn text-xs text-muted-foreground hover:text-foreground"
data-website-id="<?= (int)$w['website_id'] ?>">
Refresh
</button>
</div>
</div>
<?php if (!empty($w['client_name'])): ?>
<p class="text-xs text-muted-foreground mb-2"><?= sanitize($w['client_name']) ?></p>
<?php endif; ?>
<div class="flex items-center gap-2 text-xs mb-3" title="<?= $hasConnection ? 'Connection method set (Maintenance secret or Application Password)' : 'No connection method set — add in website settings' ?>">
<?php if ($hasConnection): ?>
<span class="w-4 h-4 rounded-full bg-emerald-500 flex items-center justify-center flex-shrink-0" aria-hidden="true">
<svg class="w-2.5 h-2.5 text-white" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/></svg>
</span>
<span class="text-muted-foreground">Connected</span>
<?php else: ?>
<span class="w-4 h-4 rounded-full bg-red-500 flex items-center justify-center flex-shrink-0" aria-hidden="true">
<svg class="w-2.5 h-2.5 text-white" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/></svg>
</span>
<span class="text-muted-foreground">No connection</span>
<?php endif; ?>
</div>
<div class="flex items-center gap-4 text-sm">
<span class="flex items-center gap-1" title="Core updates">
<svg class="w-4 h-4 text-muted-foreground" 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>
<span class="font-mono"><?= $w['core_updates_count'] ?></span>
</span>
<span class="flex items-center gap-1" title="Plugin updates">
<svg class="w-4 h-4 text-muted-foreground" 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>
<span class="font-mono"><?= $w['plugin_updates_count'] ?></span>
</span>
<span class="flex items-center gap-1" title="Theme updates">
<svg class="w-4 h-4 text-muted-foreground" 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>
<span class="font-mono"><?= $w['theme_updates_count'] ?></span>
</span>
</div>
</div>
</a>
<?php endforeach; ?>
</div>
<?php endif; ?>
<script>
(function () {
document.querySelectorAll('.site-refresh-btn').forEach(function (btn) {
btn.addEventListener('click', function (e) {
e.preventDefault();
e.stopPropagation(); // don’t follow the card link
var websiteId = parseInt(btn.getAttribute('data-website-id'), 10);
if (!websiteId) return;
var origText = btn.textContent;
btn.disabled = true;
btn.textContent = 'Refreshing…';
fetch('api/refresh-update-status.php?website_id=' + websiteId)
.then(function (r) { return r.json(); })
.then(function (data) {
btn.disabled = false;
btn.textContent = origText;
if (data.success) {
if (typeof showToast === 'function') {
showToast(data.message || 'Update status refreshed');
} else {
alert(data.message || 'Update status refreshed');
}
setTimeout(function () { window.location.reload(); }, 800);
} else {
if (typeof showToast === 'function') {
showToast(data.error || 'Refresh failed', 'error');
} else {
alert(data.error || 'Refresh failed');
}
}
})
.catch(function () {
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'; ?>