Phase 3 v2: all step automations + cross-cutting UI

Step automations (six more):
- Step 3 (Core): current vs latest WP version, db upgrade flag, auto-
  update policy, safe-update sequence reminder.
- Step 5 (Theme): parent/child detection, customisation warning when
  non-default theme without child, theme update available, inactive
  theme list.
- Step 6 (Visual): mShots screenshot URL, key-page HEAD checks (home,
  login, posts page, WC shop/cart/checkout), mixed-content scan.
- Step 7 (Performance): keyless PageSpeed Insights v5 API (mobile +
  desktop, cached 12h, skipped on .local), caching plugin detection,
  heavy-image scan (>500KB).
- Step 10 (Uptime): monitoring plugin detection (ManageWP, MainWP,
  Jetpack, WP Umbrella, UptimeRobot), www/non-www canonical check.
- Step 12 (Wrap-up): cross-step rollup — bad/warn counts, blocked
  steps, top examples for the technician's final glance.

Cross-cutting:
- Sticky step-index sidebar with status dots per step (the linear-
  stepper alternative that keeps the overview).
- 'Stop & escalate' summary card at top listing blocked steps with
  escalation guidance and notes.
- Previous-session snapshot stored on finish; diff banner on the next
  session shows new/resolved/changed counts.
- HTML report builder (printable, inline-styled). Download HTML,
  Download Markdown, Copy, and Email actions on the finish panel.
  Email uses wp_mail with text/html.

Smoke-tested on testsite: all 12 steps return findings (5/9/4 by level
on a fresh local install), admin page renders with all UI markers,
HTML report is 26KB, Markdown report is 13KB, prev-session diff banner
appears on second session.

Deferred:
- hc-5ix.27 self-hosted update channel — needs hosting infra.
- Full PDF report — would need vendoring Dompdf.
- Step 3 safe-mode update wizard — worth its own bead.
This commit is contained in:
2026-06-11 16:13:15 +01:00
parent 0d51fc3b59
commit cf3007ec37
10 changed files with 706 additions and 20 deletions

View File

@@ -93,6 +93,115 @@ function wph_build_markdown_report(WPH_Session $session): string {
return implode("\n", $lines) . "\n";
}
function wph_build_html_report(WPH_Session $session): string {
$tech = get_userdata($session->technician_id());
$tech_name = $tech ? $tech->display_name : '#' . $session->technician_id();
ob_start();
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Site Healthcheck — <?php echo esc_html($session->site_url()); ?></title>
<style>
body { font: 14px/1.55 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; color: #1d1d1f; background: #f7f7f8; margin: 0; padding: 2rem; }
.wrap { max-width: 820px; margin: 0 auto; background: #fff; border-radius: 10px; padding: 2rem 2.4rem; box-shadow: 0 1px 3px rgba(0,0,0,.06); }
h1 { margin: 0 0 .25rem; font-size: 1.6rem; }
h2 { margin: 1.5rem 0 .5rem; font-size: 1.15rem; border-bottom: 1px solid #e5e5ea; padding-bottom: .25rem; }
h3 { margin: 1.2rem 0 .35rem; font-size: 1rem; }
table { width: 100%; border-collapse: collapse; font-size: .92em; margin: .4rem 0; }
th, td { text-align: left; padding: .35rem .55rem; border-bottom: 1px solid #f0f0f1; vertical-align: top; }
th { background: #fafafb; font-weight: 600; }
.meta { color: #646970; }
.pill { display: inline-block; padding: 1px 8px; border-radius: 10px; font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: .04em; }
.pill.done { background: #def7e3; color: #155724; }
.pill.skipped { background: #fff3cd; color: #856404; }
.pill.blocked { background: #fbeae8; color: #721c24; }
.pill.n_a { background: #e2e3e5; color: #41464b; }
.pill.not_started { background: #f0f0f1; color: #646970; }
blockquote { margin: .5rem 0; padding: .35rem .9rem; border-left: 3px solid #c3c4c7; background: #fafafa; color: #444; }
ul.findings { list-style: none; padding-left: 0; }
ul.findings li { padding: .15rem 0; }
.lvl-ok::before { content: '✓ '; color: #1a8917; font-weight: 700; }
.lvl-warn::before { content: '⚠ '; color: #b07a00; font-weight: 700; }
.lvl-bad::before { content: '✗ '; color: #c0392b; font-weight: 700; }
.lvl-info::before { content: '· '; color: #646970; font-weight: 700; }
.detail { color: #646970; font-size: .92em; }
footer { margin-top: 2rem; color: #646970; font-size: .85em; text-align: center; }
</style>
</head>
<body>
<div class="wrap">
<h1>Site Healthcheck</h1>
<p class="meta"><?php echo esc_html($session->site_url()); ?></p>
<table>
<tr><th>Started</th><td><?php echo esc_html(date('Y-m-d H:i', $session->started_at())); ?></td></tr>
<?php if ($session->is_finished()): $mins = max(1, (int) round(((int) $session->finished_at() - $session->started_at()) / 60)); ?>
<tr><th>Finished</th><td><?php echo esc_html(date('Y-m-d H:i', (int) $session->finished_at())); ?> (≈<?php echo $mins; ?> min)</td></tr>
<?php endif; ?>
<tr><th>Technician</th><td><?php echo esc_html($tech_name); ?></td></tr>
<tr><th>WordPress</th><td><?php echo esc_html($session->wp_version()); ?></td></tr>
<tr><th>PHP</th><td><?php echo esc_html($session->php_version()); ?></td></tr>
</table>
<h2>Summary</h2>
<table>
<thead><tr><th>Step</th><th>Status</th></tr></thead>
<tbody>
<?php foreach (WPH_Steps::instance()->all() as $step):
$state = $session->step_state($step->id());
?>
<tr>
<td><?php echo esc_html($step->title()); ?></td>
<td><span class="pill <?php echo esc_attr($state['status']); ?>"><?php echo esc_html(str_replace('_', ' ', $state['status'])); ?></span></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<h2>Detail</h2>
<?php foreach (WPH_Steps::instance()->all() as $step):
$state = $session->step_state($step->id());
$auto = $session->get_autocheck($step->id());
?>
<h3><?php echo esc_html($step->title()); ?></h3>
<p><span class="pill <?php echo esc_attr($state['status']); ?>"><?php echo esc_html(str_replace('_', ' ', $state['status'])); ?></span></p>
<?php if ($blurb = $step->blurb()): ?>
<p><?php echo esc_html($blurb); ?></p>
<?php endif; ?>
<?php if ($items = $step->sub_items()): ?>
<ul>
<?php foreach ($items as $i): ?>
<li><?php echo esc_html($i); ?></li>
<?php endforeach; ?>
</ul>
<?php endif; ?>
<?php if (!empty($state['notes'])): ?>
<blockquote><?php echo nl2br(esc_html($state['notes'])); ?></blockquote>
<?php endif; ?>
<?php if ($auto && !empty($auto['findings'])): ?>
<ul class="findings">
<?php foreach ($auto['findings'] as $f): ?>
<li class="lvl-<?php echo esc_attr($f['level']); ?>">
<strong><?php echo esc_html($f['label']); ?></strong>
<?php if ($f['value'] !== ''): ?>— <?php echo esc_html($f['value']); ?><?php endif; ?>
<?php if ($f['detail'] !== ''): ?><span class="detail"> — <?php echo esc_html($f['detail']); ?></span><?php endif; ?>
</li>
<?php endforeach; ?>
</ul>
<?php endif; ?>
<?php endforeach; ?>
<footer>Generated by Site Healthcheck plugin v<?php echo esc_html(WPH_VERSION); ?> · <?php echo esc_html(date('Y-m-d H:i')); ?></footer>
</div>
</body>
</html>
<?php
return (string) ob_get_clean();
}
function wph_status_label(string $status): string {
switch ($status) {
case WPH_Session::STATUS_DONE: return '✅ Done';