Files
wp-healthcheck/includes/report.php
Steve Hanlon cf3007ec37 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.
2026-06-11 16:13:15 +01:00

215 lines
9.2 KiB
PHP
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
if (!defined('ABSPATH')) exit;
/**
* Build a Markdown report for a healthcheck session.
* Per bead hc-5ix.7: pure Markdown, downloadable. No DB-side report storage
* (plugin may be uninstalled at the end of the engagement).
*/
function wph_build_markdown_report(WPH_Session $session): string {
$tech = get_userdata($session->technician_id());
$tech_name = $tech ? $tech->display_name : '#' . $session->technician_id();
$lines = [];
$lines[] = '# Site Healthcheck — ' . $session->site_url();
$lines[] = '';
$lines[] = '- **Started:** ' . date('Y-m-d H:i', $session->started_at());
if ($session->is_finished()) {
$lines[] = '- **Finished:** ' . date('Y-m-d H:i', (int) $session->finished_at());
$minutes = max(1, (int) round(((int) $session->finished_at() - $session->started_at()) / 60));
$lines[] = '- **Duration:** ~' . $minutes . ' minute(s)';
}
$lines[] = '- **Technician:** ' . $tech_name;
$lines[] = '- **Site:** ' . $session->site_url();
$lines[] = '- **WordPress:** ' . $session->wp_version();
$lines[] = '- **PHP:** ' . $session->php_version();
$lines[] = '- **Session ID:** ' . $session->id();
$lines[] = '';
// Summary table
$lines[] = '## Summary';
$lines[] = '';
$lines[] = '| Step | Status |';
$lines[] = '|---|---|';
foreach (WPH_Steps::instance()->all() as $step) {
$state = $session->step_state($step->id());
$lines[] = '| ' . $step->title() . ' | ' . wph_status_label($state['status']) . ' |';
}
$lines[] = '';
// Per-step detail
$lines[] = '## Detail';
$lines[] = '';
foreach (WPH_Steps::instance()->all() as $step) {
$state = $session->step_state($step->id());
$lines[] = '### ' . $step->title();
$lines[] = '';
$lines[] = '_Status: ' . wph_status_label($state['status']) . '_';
if ($state['updated_at']) {
$lines[] = '_Saved: ' . date('Y-m-d H:i', $state['updated_at']) . '_';
}
if ($blurb = $step->blurb()) {
$lines[] = '';
$lines[] = $blurb;
}
if ($items = $step->sub_items()) {
$lines[] = '';
foreach ($items as $item) {
$lines[] = '- [ ] ' . $item;
}
}
if (!empty($state['notes'])) {
$lines[] = '';
$lines[] = '**Notes:**';
$lines[] = '';
foreach (preg_split('/\R/', (string) $state['notes']) as $nl) {
$lines[] = '> ' . $nl;
}
}
$auto = $session->get_autocheck($step->id());
if ($auto && !empty($auto['findings'])) {
$lines[] = '';
$lines[] = '**Automated checks** (run ' . date('Y-m-d H:i', (int) ($auto['checked_at'] ?? 0)) . '):';
$lines[] = '';
foreach ($auto['findings'] as $finding) {
$icon = ['ok' => '✅', 'warn' => '⚠️', 'bad' => '❌', 'info' => ''][$finding['level']] ?? '·';
$bits = [$icon, '**' . $finding['label'] . '**'];
if (!empty($finding['value'])) $bits[] = $finding['value'];
if (!empty($finding['detail'])) $bits[] = '— ' . $finding['detail'];
$lines[] = '- ' . implode(' ', $bits);
}
}
if ($state['status'] === WPH_Session::STATUS_BLOCKED && ($esc = $step->escalation())) {
$lines[] = '';
$lines[] = '> ⚠ **Escalation:** ' . $esc;
}
$lines[] = '';
}
$lines[] = '---';
$lines[] = '_Generated by Site Healthcheck plugin v' . WPH_VERSION . '_';
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';
case WPH_Session::STATUS_SKIPPED: return '⏭ Skipped';
case WPH_Session::STATUS_BLOCKED: return '🛑 Blocked';
case WPH_Session::STATUS_NA: return '— N/A';
case WPH_Session::STATUS_NOT_STARTED:
default: return '◻ Not started';
}
}