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:
@@ -8,6 +8,8 @@ add_action('admin_post_wph_finish', 'wph_handle_finish');
|
||||
add_action('admin_post_wph_discard', 'wph_handle_discard');
|
||||
add_action('admin_post_wph_download_report', 'wph_handle_download_report');
|
||||
add_action('admin_post_wph_refresh_checks', 'wph_handle_refresh_checks');
|
||||
add_action('admin_post_wph_download_html', 'wph_handle_download_html');
|
||||
add_action('admin_post_wph_email_report', 'wph_handle_email_report');
|
||||
add_action('admin_enqueue_scripts', 'wph_enqueue_assets');
|
||||
|
||||
function wph_register_menu(): void {
|
||||
@@ -68,6 +70,24 @@ function wph_inline_css(): string {
|
||||
.wph-autocheck .value { color:#1d1d1f; }
|
||||
.wph-autocheck .detail { color:#646970; font-size:.9em; }
|
||||
.wph-checked-at { color:#646970; font-size:.85em; }
|
||||
.wph-layout { display:grid; grid-template-columns: 220px 1fr; gap:1rem; }
|
||||
.wph-sidebar { position:sticky; top:36px; align-self:start; max-height: calc(100vh - 60px); overflow:auto; }
|
||||
.wph-sidebar .wph-card { padding:.75rem 1rem; }
|
||||
.wph-sidebar h3 { margin:0 0 .4rem; font-size:.9rem; text-transform:uppercase; letter-spacing:.04em; color:#646970; }
|
||||
.wph-sidebar ol { margin:0; padding:0; list-style:none; }
|
||||
.wph-sidebar li { padding:.18rem 0; }
|
||||
.wph-sidebar a { text-decoration:none; }
|
||||
.wph-sidebar .dot { display:inline-block; width:.6rem; height:.6rem; border-radius:50%; margin-right:.4rem; background:#dcdcde; vertical-align:middle; }
|
||||
.wph-sidebar .dot-done { background:#1a8917; }
|
||||
.wph-sidebar .dot-skipped { background:#b07a00; }
|
||||
.wph-sidebar .dot-blocked { background:#c0392b; }
|
||||
.wph-sidebar .dot-n_a { background:#646970; }
|
||||
.wph-diff { background:#eef4fb; border:1px solid #cfe0f3; padding:.6rem 1rem; border-radius:6px; margin:.5rem 0; font-size:.9em; }
|
||||
.wph-diff strong { display:inline-block; margin-right:.4rem; }
|
||||
.wph-diff .delta-new { color:#c0392b; }
|
||||
.wph-diff .delta-resolved { color:#1a8917; }
|
||||
.wph-diff .delta-changed { color:#b07a00; }
|
||||
@media (max-width: 980px) { .wph-layout { grid-template-columns: 1fr; } .wph-sidebar { position: static; max-height: none; } }
|
||||
';
|
||||
}
|
||||
|
||||
@@ -138,10 +158,96 @@ function wph_render_active_session(WPH_Session $session): void {
|
||||
<?php
|
||||
|
||||
WPH_Recovery_Bootstrap::render_status();
|
||||
wph_render_blocked_summary($session);
|
||||
wph_render_diff_summary($session);
|
||||
|
||||
echo '<div class="wph-layout">';
|
||||
wph_render_sidebar($session);
|
||||
echo '<div>';
|
||||
foreach (WPH_Steps::instance()->all() as $step) {
|
||||
wph_render_step_card($session, $step);
|
||||
}
|
||||
echo '</div></div>';
|
||||
}
|
||||
|
||||
function wph_render_sidebar(WPH_Session $session): void {
|
||||
echo '<aside class="wph-sidebar"><div class="wph-card"><h3>Steps</h3><ol>';
|
||||
foreach (WPH_Steps::instance()->all() as $step) {
|
||||
$state = $session->step_state($step->id());
|
||||
$cls = 'dot-' . $state['status'];
|
||||
printf(
|
||||
'<li><span class="dot %s"></span><a href="#step-%s">%s</a></li>',
|
||||
esc_attr($cls),
|
||||
esc_attr($step->id()),
|
||||
esc_html($step->title())
|
||||
);
|
||||
}
|
||||
echo '</ol></div></aside>';
|
||||
}
|
||||
|
||||
function wph_render_blocked_summary(WPH_Session $session): void {
|
||||
$blocked = [];
|
||||
foreach (WPH_Steps::instance()->all() as $step) {
|
||||
$state = $session->step_state($step->id());
|
||||
if ($state['status'] === WPH_Session::STATUS_BLOCKED) {
|
||||
$blocked[] = ['step' => $step, 'state' => $state];
|
||||
}
|
||||
}
|
||||
if (!$blocked) return;
|
||||
echo '<div class="wph-card" style="border-left:4px solid #c0392b">';
|
||||
echo '<h2 style="color:#721c24">Stop & escalate</h2>';
|
||||
echo '<p>The following steps are blocked. Resolve or escalate before continuing:</p>';
|
||||
echo '<ul style="margin-left:1.25rem">';
|
||||
foreach ($blocked as $b) {
|
||||
$esc = $b['step']->escalation();
|
||||
echo '<li><a href="#step-' . esc_attr($b['step']->id()) . '"><strong>' . esc_html($b['step']->title()) . '</strong></a>';
|
||||
if ($esc) echo ' — ' . esc_html($esc);
|
||||
if (!empty($b['state']['notes'])) echo '<br><em>' . esc_html($b['state']['notes']) . '</em>';
|
||||
echo '</li>';
|
||||
}
|
||||
echo '</ul></div>';
|
||||
}
|
||||
|
||||
function wph_render_diff_summary(WPH_Session $session): void {
|
||||
$prev = WPH_Session::previous();
|
||||
if (!$prev) return;
|
||||
|
||||
// Aggregate findings by step+id from each session
|
||||
$current_idx = [];
|
||||
foreach (WPH_Steps::instance()->all() as $step) {
|
||||
$r = $session->get_autocheck($step->id());
|
||||
if (!$r) continue;
|
||||
foreach ($r['findings'] as $f) {
|
||||
$current_idx[$step->id() . '|' . $f['id']] = $f;
|
||||
}
|
||||
}
|
||||
$prev_idx = [];
|
||||
foreach (WPH_Steps::instance()->all() as $step) {
|
||||
$r = $prev->get_autocheck($step->id());
|
||||
if (!$r) continue;
|
||||
foreach ($r['findings'] as $f) {
|
||||
$prev_idx[$step->id() . '|' . $f['id']] = $f;
|
||||
}
|
||||
}
|
||||
$new = $resolved = $changed = [];
|
||||
foreach ($current_idx as $k => $f) {
|
||||
if (!isset($prev_idx[$k])) {
|
||||
if (in_array($f['level'], ['warn', 'bad'], true)) $new[] = $f;
|
||||
} elseif ($prev_idx[$k]['level'] !== $f['level'] || $prev_idx[$k]['value'] !== $f['value']) {
|
||||
$changed[] = ['was' => $prev_idx[$k], 'now' => $f];
|
||||
}
|
||||
}
|
||||
foreach ($prev_idx as $k => $f) {
|
||||
if (!isset($current_idx[$k]) && in_array($f['level'], ['warn', 'bad'], true)) {
|
||||
$resolved[] = $f;
|
||||
}
|
||||
}
|
||||
if (!$new && !$resolved && !$changed) return;
|
||||
echo '<div class="wph-diff"><strong>Δ vs. previous session</strong> (finished ' . esc_html(date('Y-m-d', (int) $prev->finished_at())) . ')';
|
||||
if ($new) echo ' · <span class="delta-new">' . count($new) . ' new issue(s)</span>';
|
||||
if ($resolved) echo ' · <span class="delta-resolved">' . count($resolved) . ' resolved</span>';
|
||||
if ($changed) echo ' · <span class="delta-changed">' . count($changed) . ' changed</span>';
|
||||
echo '</div>';
|
||||
}
|
||||
|
||||
function wph_render_step_card(WPH_Session $session, WPH_Step $step): void {
|
||||
@@ -207,21 +313,33 @@ function wph_render_step_card(WPH_Session $session, WPH_Step $step): void {
|
||||
|
||||
function wph_render_finished_panel(WPH_Session $session): void {
|
||||
$report = wph_build_markdown_report($session);
|
||||
$admin_email = get_option('admin_email');
|
||||
?>
|
||||
<div class="wph-card">
|
||||
<h2>Healthcheck finished</h2>
|
||||
<p>Started <?php echo esc_html(date('Y-m-d H:i', $session->started_at())); ?> · Finished <?php echo esc_html(date('Y-m-d H:i', (int) $session->finished_at())); ?></p>
|
||||
<div class="wph-actions">
|
||||
<form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>">
|
||||
<form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>" style="display:inline">
|
||||
<?php wp_nonce_field('wph_download_report'); ?>
|
||||
<input type="hidden" name="action" value="wph_download_report">
|
||||
<button class="button button-primary">Download Markdown report</button>
|
||||
<button class="button button-primary">Download Markdown</button>
|
||||
</form>
|
||||
<form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>" style="display:inline">
|
||||
<?php wp_nonce_field('wph_download_html'); ?>
|
||||
<input type="hidden" name="action" value="wph_download_html">
|
||||
<button class="button">Download HTML</button>
|
||||
</form>
|
||||
<button class="button" onclick="navigator.clipboard.writeText(document.getElementById('wph-report-md').textContent);this.textContent='Copied!';setTimeout(()=>this.textContent='Copy Markdown',2000)">Copy Markdown</button>
|
||||
<form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>" style="display:inline">
|
||||
<?php wp_nonce_field('wph_email_report'); ?>
|
||||
<input type="hidden" name="action" value="wph_email_report">
|
||||
<input type="email" name="to" placeholder="recipient@example.com" value="<?php echo esc_attr($admin_email); ?>" required style="min-width:14em">
|
||||
<button class="button">Email report</button>
|
||||
</form>
|
||||
<button class="button" onclick="navigator.clipboard.writeText(document.getElementById('wph-report-md').textContent);this.textContent='Copied!';setTimeout(()=>this.textContent='Copy to clipboard',2000)">Copy to clipboard</button>
|
||||
<form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>" onsubmit="return confirm('Discard this finished session?');" style="display:inline">
|
||||
<?php wp_nonce_field('wph_discard'); ?>
|
||||
<input type="hidden" name="action" value="wph_discard">
|
||||
<button class="button button-link-delete">Discard & start a new one</button>
|
||||
<button class="button button-link-delete">Discard</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -329,14 +447,45 @@ function wph_handle_download_report(): void {
|
||||
check_admin_referer('wph_download_report');
|
||||
$session = WPH_Session::current();
|
||||
if (!$session) wp_die('No session.');
|
||||
$report = wph_build_markdown_report($session);
|
||||
wph_stream_report($session, 'md');
|
||||
}
|
||||
|
||||
function wph_handle_download_html(): void {
|
||||
if (!current_user_can('manage_options')) wp_die('Forbidden');
|
||||
check_admin_referer('wph_download_html');
|
||||
$session = WPH_Session::current();
|
||||
if (!$session) wp_die('No session.');
|
||||
wph_stream_report($session, 'html');
|
||||
}
|
||||
|
||||
function wph_stream_report(WPH_Session $session, string $format): void {
|
||||
$host = parse_url(get_site_url(), PHP_URL_HOST) ?: 'site';
|
||||
$host = preg_replace('/[^a-z0-9.-]/i', '', (string) $host);
|
||||
$stamp = date('Ymd', $session->started_at() ?: time());
|
||||
$filename = 'wph-report-' . $host . '-' . $stamp . '.md';
|
||||
nocache_headers();
|
||||
header('Content-Type: text/markdown; charset=UTF-8');
|
||||
header('Content-Disposition: attachment; filename="' . $filename . '"');
|
||||
echo $report;
|
||||
if ($format === 'html') {
|
||||
header('Content-Type: text/html; charset=UTF-8');
|
||||
header('Content-Disposition: attachment; filename="wph-report-' . $host . '-' . $stamp . '.html"');
|
||||
echo wph_build_html_report($session);
|
||||
} else {
|
||||
header('Content-Type: text/markdown; charset=UTF-8');
|
||||
header('Content-Disposition: attachment; filename="wph-report-' . $host . '-' . $stamp . '.md"');
|
||||
echo wph_build_markdown_report($session);
|
||||
}
|
||||
exit;
|
||||
}
|
||||
|
||||
function wph_handle_email_report(): void {
|
||||
if (!current_user_can('manage_options')) wp_die('Forbidden');
|
||||
check_admin_referer('wph_email_report');
|
||||
$session = WPH_Session::current();
|
||||
if (!$session) wp_die('No session.');
|
||||
$to = isset($_POST['to']) ? sanitize_email((string) wp_unslash($_POST['to'])) : '';
|
||||
if (!is_email($to)) wp_die('Bad email address.');
|
||||
$host = parse_url(get_site_url(), PHP_URL_HOST) ?: 'site';
|
||||
$subject = 'Site Healthcheck — ' . $host . ' — ' . date('Y-m-d', $session->started_at() ?: time());
|
||||
$html = wph_build_html_report($session);
|
||||
$ok = wp_mail($to, $subject, $html, ['Content-Type: text/html; charset=UTF-8']);
|
||||
wp_safe_redirect(admin_url('tools.php?page=site-healthcheck&wph_mail=' . ($ok ? '1' : '0')));
|
||||
exit;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user