'; echo '

Site Healthcheck

'; if ($cfg_err = ATT_HC_Api::config_error()) { $settings_url = admin_url('tools.php?page=att-site-healthcheck-settings'); echo '

Central history server not usable: ' . esc_html($cfg_err) . '

'; echo '

Open settings to set the server URL and API key, or add the following to wp-config.php:

'; echo '
define(\'ATT_HC_API_URL\', \'https://your-history-server.example.com\');' . "\n" . 'define(\'ATT_HC_API_KEY\', \'<shared secret>\');
'; echo ''; return; } if ($msg = get_transient('att_hc_install_message')) { delete_transient('att_hc_install_message'); echo '

' . esc_html($msg) . '

'; } if ($err = get_transient('att_hc_api_error')) { delete_transient('att_hc_api_error'); echo '

Central history server: ' . esc_html($err) . '

'; } // Local (non-server) problems — bad input and the like. if ($err = get_transient('att_hc_error')) { delete_transient('att_hc_error'); echo '

' . esc_html($err) . '

'; } if (!$session) { att_hc_render_start_panel(); echo ''; return; } if ($session->is_finished()) { att_hc_render_finished_panel($session); echo ''; return; } att_hc_render_active_session($session); echo ''; } function att_hc_render_start_panel(): void { // Site key for the lookup is either user-specified (?site_key=...) or // the normalised current site URL. The technician can override on submit. $default_key = ATT_HC_Session::normalise_site_url(get_site_url()); $site_key = isset($_GET['site_key']) && is_string($_GET['site_key']) && $_GET['site_key'] !== '' ? sanitize_text_field(wp_unslash((string) $_GET['site_key'])) : $default_key; // Best-effort fetches — surface a notice on failure but still let the // tech start a fresh session. (Resume needs a successful list call to // know which session to resume.) $incomplete = []; $recent = []; $lookup_err = null; try { $resp = ATT_HC_Api::list_healthchecks($site_key, false, 20); foreach ($resp['healthchecks'] ?? [] as $hc) { if (empty($hc['finished_at'])) $incomplete[] = $hc; } } catch (ATT_HC_Api_Exception $e) { $lookup_err = $e->getMessage(); } try { $sites = ATT_HC_Api::recent_sites(20); $recent = $sites['sites'] ?? []; } catch (ATT_HC_Api_Exception $e) { // No-op — the recent dropdown is just a convenience. } ?>

Start a healthcheck

Could not look up history for this engagement: . You can still start a new session.


Reset to this site

Used to group runs that span dev + live for the same engagement. Defaults to the host of this site. Type to autocomplete from recent engagement(s).

In-progress healthchecks for

Pick up where a previous session left off, or start a fresh one below.

StartedLast activityReporting URLAction
ago
progress(); $tech = get_userdata($session->technician_id()); ?>

Session: id()); ?> · Started: started_at())); ?> · Technician: display_name : '#' . $session->technician_id()); ?> · / steps

WP wp_version()); ?> · PHP php_version()); ?> · Site site_url()); ?>

site_key(), $session->id()); $step_counts = $resp['counts'] ?? []; } catch (ATT_HC_Api_Exception $e) { // History badges are a nice-to-have — don't block the page. } att_hc_print_step_history_assets($session); echo '
'; att_hc_render_sidebar($session); echo '
'; foreach (ATT_HC_Steps::instance()->all() as $step) { att_hc_render_step_card($session, $step, (int) ($step_counts[$step->id()] ?? 0)); } echo '
'; } /** * "Next healthcheck due" control. Shown on both the active and the finished * panel — the point at which you know when to come back is often wrap-up, which * may be after the report has already been generated. */ function att_hc_render_next_due_form(ATT_HC_Session $session): void { $due = $session->next_due(); ?>
Shown against this site on the healthcheck dashboard.
admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('att_hc_step_history'), 'reportingUrl' => $session->reporting_url(), ]; ?>

Steps

    '; foreach (ATT_HC_Steps::instance()->all() as $step) { $state = $session->step_state($step->id()); $cls = 'dot-' . $state['status']; printf( '
  1. %s
  2. ', esc_attr($cls), esc_attr($step->id()), esc_html($step->title()) ); } echo '
'; } function att_hc_render_blocked_summary(ATT_HC_Session $session): void { $blocked = []; foreach (ATT_HC_Steps::instance()->all() as $step) { $state = $session->step_state($step->id()); if ($state['status'] === ATT_HC_Session::STATUS_BLOCKED) { $blocked[] = ['step' => $step, 'state' => $state]; } } if (!$blocked) return; echo '
'; echo '

Stop & escalate

'; echo '

The following steps are blocked. Resolve or escalate before continuing:

'; echo '
'; } function att_hc_render_diff_summary(ATT_HC_Session $session): void { $prev = ATT_HC_Session::previous(); if (!$prev) return; // Aggregate findings by step+id from each session $current_idx = []; foreach (ATT_HC_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 (ATT_HC_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 '
Δ vs. previous session (finished ' . esc_html(date('Y-m-d', (int) $prev->finished_at())) . ')'; if ($new) echo ' · ' . count($new) . ' new issue(s)'; if ($resolved) echo ' · ' . count($resolved) . ' resolved'; if ($changed) echo ' · ' . count($changed) . ' changed'; echo '
'; } function att_hc_render_step_card(ATT_HC_Session $session, ATT_HC_Step $step, int $history_count = 0): void { $state = $session->step_state($step->id()); $status = $state['status']; $notes = $state['notes']; ?>

title()); ?>

0): ?>
Previous notes for this step ()
blurb()): ?>

sub_items()): ?> watch_outs()): ?>
Watch out for:
escalation())): ?>
render_extra($session->data()); ?>
id()); ?>

Last saved ago

Healthcheck finished

Started started_at())); ?> · Finished finished_at())); ?>

Report preview

getMessage(), 60); } wp_safe_redirect(admin_url('tools.php?page=att-site-healthcheck')); exit; } function att_hc_handle_save_next_due(): void { if (!current_user_can('manage_options')) wp_die('Forbidden'); check_admin_referer('att_hc_save_next_due'); $redirect = admin_url('tools.php?page=att-site-healthcheck'); $session = ATT_HC_Session::current(); if (!$session) wp_die('No active session.'); $clear = !empty($_POST['next_due_clear']); $raw = isset($_POST['next_due']) ? sanitize_text_field(wp_unslash((string) $_POST['next_due'])) : ''; $date = $clear ? null : ATT_HC_Session::sanitise_due_date($raw); // Distinguish "cleared the field" (legitimate) from "typed something that // isn't a date" — silently clearing on bad input would lose the existing date. if (!$clear && $raw !== '' && $date === null) { set_transient('att_hc_error', 'Could not save the next healthcheck date: "' . $raw . '" is not a valid date (expected YYYY-MM-DD).', 60); wp_safe_redirect($redirect); exit; } try { $session->set_next_due($date); set_transient( 'att_hc_install_message', $date === null ? 'Next healthcheck date cleared.' : 'Next healthcheck due ' . $date . '.', 60 ); } catch (ATT_HC_Api_Exception $e) { set_transient('att_hc_api_error', 'Could not save the next healthcheck date: ' . $e->getMessage(), 60); } wp_safe_redirect($redirect); exit; } function att_hc_handle_step_history_ajax(): void { if (!current_user_can('manage_options')) wp_send_json_error('Forbidden', 403); if (!check_ajax_referer('att_hc_step_history', 'nonce', false)) { wp_send_json_error('Bad nonce', 403); } $step_id = isset($_POST['step_id']) ? sanitize_key((string) $_POST['step_id']) : ''; if ($step_id === '') wp_send_json_error('Missing step_id', 400); $session = ATT_HC_Session::current(); if (!$session) wp_send_json_error('No active session', 400); try { $resp = ATT_HC_Api::step_history($step_id, $session->site_key(), 5, $session->id()); wp_send_json_success($resp); } catch (ATT_HC_Api_Exception $e) { wp_send_json_error($e->getMessage(), 502); } } function att_hc_handle_resume(): void { if (!current_user_can('manage_options')) wp_die('Forbidden'); $id = isset($_POST['id']) ? sanitize_text_field(wp_unslash((string) $_POST['id'])) : ''; check_admin_referer('att_hc_resume_' . $id); if (ATT_HC_Session::current()) { // Same guard as start — don't clobber an active session on a stray click. wp_safe_redirect(admin_url('tools.php?page=att-site-healthcheck')); exit; } try { ATT_HC_Session::resume($id); } catch (ATT_HC_Api_Exception $e) { set_transient('att_hc_api_error', 'Could not resume that session: ' . $e->getMessage(), 60); } wp_safe_redirect(admin_url('tools.php?page=att-site-healthcheck')); exit; } function att_hc_handle_save_step(): void { if (!current_user_can('manage_options')) wp_die('Forbidden'); $step_id = isset($_POST['step']) ? sanitize_key((string) $_POST['step']) : ''; check_admin_referer('att_hc_save_step_' . $step_id); $session = ATT_HC_Session::current(); if (!$session || $session->is_finished()) wp_die('No active session.'); if (!ATT_HC_Steps::instance()->get($step_id)) wp_die('Unknown step.'); $status = isset($_POST['status']) ? sanitize_key((string) $_POST['status']) : ATT_HC_Session::STATUS_NOT_STARTED; $notes = isset($_POST['notes']) ? wp_unslash((string) $_POST['notes']) : ''; try { $session->update_step($step_id, $status, $notes); } catch (ATT_HC_Api_Exception $e) { set_transient('att_hc_api_error', 'Step not saved (central server rejected the write): ' . $e->getMessage(), 60); } wp_safe_redirect(admin_url('tools.php?page=att-site-healthcheck#step-' . rawurlencode($step_id))); exit; } function att_hc_handle_finish(): void { if (!current_user_can('manage_options')) wp_die('Forbidden'); check_admin_referer('att_hc_finish'); $session = ATT_HC_Session::current(); if (!$session) wp_die('No active session.'); try { $session->finish(); } catch (ATT_HC_Api_Exception $e) { set_transient('att_hc_api_error', 'Could not mark session finished on the central server: ' . $e->getMessage(), 60); } wp_safe_redirect(admin_url('tools.php?page=att-site-healthcheck')); exit; } function att_hc_handle_discard(): void { if (!current_user_can('manage_options')) wp_die('Forbidden'); check_admin_referer('att_hc_discard'); ATT_HC_Session::discard(); wp_safe_redirect(admin_url('tools.php?page=att-site-healthcheck')); exit; } function att_hc_render_autocheck(ATT_HC_Session $session, ATT_HC_Step $step): void { if (!$step->has_autocheck()) return; $result = $session->get_autocheck($step->id()); ?>

Automated checks

id()); ?>

No automated checks have been run for this step yet.

'✓', 'warn' => '⚠', 'bad' => '✗', 'info' => '·'][$finding['level']] ?? '·'; ?>

Checked ago ()

Site Healthcheck — Settings

← Back to healthcheck

Saved.

Central history server

The plugin writes every step to a central server so healthcheck history survives per-engagement plugin uninstalls and follows the site across dev/live URLs.

Each field can be set via a constant in wp-config.php (then it takes precedence and the field below is locked). Using constants is preferred where the host allows it — the API key stored as an option is visible to WP admins and anyone with DB access.

>

Set via ATT_HC_API_URL constant.

Must start with https://. No trailing slash necessary.

>

Set via ATT_HC_API_KEY constant.

Shared secret. Copy it from the server's config.php. Stored in the WP options table.

Recovery plugin source (Gitea)

One-click install pulls site-recovery from a private Gitea repo. The token needs read access to the repo only — a deploy / read-only PAT is safer than a personal token.

Each field can be set via a constant in wp-config.php (then it takes precedence and the field below is locked).

>

Set via ATT_HC_GITEA_HOST constant.

>

Set via ATT_HC_GITEA_OWNER constant.

>

Set via ATT_HC_GITEA_REPO constant.

>

Set via ATT_HC_GITEA_TOKEN constant.

Stored in WP options. Use a read-only deploy token scoped to this repo if possible.


Install now


✓ Site Recovery is already installed at .

(string) wp_unslash($_POST['host'] ?? ''), 'owner' => (string) wp_unslash($_POST['owner'] ?? ''), 'repo' => (string) wp_unslash($_POST['repo'] ?? ''), 'token' => (string) wp_unslash($_POST['token'] ?? ''), ]); wp_safe_redirect(admin_url('tools.php?page=att-site-healthcheck-settings&att_hc_saved=1')); exit; } function att_hc_handle_save_api_settings(): void { if (!current_user_can('manage_options')) wp_die('Forbidden'); check_admin_referer('att_hc_save_api_settings'); // Only touch options the user is actually allowed to write. If a constant // is defined for a field, we ignore the submitted value (the input is also // disabled in the UI, but belt-and-braces on the handler too). if (!ATT_HC_Api::url_from_constant()) { $url = trim((string) wp_unslash($_POST['api_url'] ?? '')); $url = rtrim($url, '/'); update_option(ATT_HC_Api::OPT_URL, $url, false); } if (!ATT_HC_Api::key_from_constant()) { $key = trim((string) wp_unslash($_POST['api_key'] ?? '')); update_option(ATT_HC_Api::OPT_KEY, $key, false); } wp_safe_redirect(admin_url('tools.php?page=att-site-healthcheck-settings&att_hc_saved=1')); exit; } function att_hc_handle_recovery_install(): void { if (!current_user_can('install_plugins') || !current_user_can('activate_plugins')) wp_die('Forbidden'); check_admin_referer('att_hc_recovery_install'); @set_time_limit(120); $result = ATT_HC_Recovery_Installer::install_and_activate(); if (is_wp_error($result)) { wp_die('Install failed: ' . esc_html($result->get_error_message()) . '

Back to settings

'); } set_transient('att_hc_install_message', sprintf('Site Recovery installed from %s "%s" and activated.', $result['ref']['type'], $result['ref']['ref']), 60); wp_safe_redirect(admin_url('tools.php?page=att-site-healthcheck')); exit; } function att_hc_handle_step_action(): void { if (!current_user_can('manage_options')) wp_die('Forbidden'); $step_id = isset($_POST['step']) ? sanitize_key((string) $_POST['step']) : ''; $action_name = isset($_POST['step_action']) ? sanitize_key((string) $_POST['step_action']) : ''; check_admin_referer('att_hc_step_action_' . $step_id . '_' . $action_name); $session = ATT_HC_Session::current(); if (!$session || $session->is_finished()) wp_die('No active session.'); $step = ATT_HC_Steps::instance()->get($step_id); if (!$step) wp_die('Unknown step.'); // Pass POST through unslashed so handlers see the raw user input. $input = wp_unslash($_POST); @set_time_limit(60); $finding = $step->handle_action($action_name, is_array($input) ? $input : []); if (is_array($finding)) { // Append (or replace by id) onto this step's stored findings. $existing = $session->get_autocheck($step_id); $findings = $existing['findings'] ?? []; $replaced = false; foreach ($findings as $i => $f) { if (($f['id'] ?? '') === ($finding['id'] ?? '')) { $findings[$i] = $finding; $replaced = true; break; } } if (!$replaced) $findings[] = $finding; try { $session->set_autocheck($step_id, $findings); } catch (ATT_HC_Api_Exception $e) { set_transient('att_hc_api_error', 'Step action ran but the result could not be saved to the central server: ' . $e->getMessage(), 60); } } wp_safe_redirect(admin_url('tools.php?page=att-site-healthcheck#step-' . rawurlencode($step_id))); exit; } function att_hc_handle_refresh_checks(): void { if (!current_user_can('manage_options')) wp_die('Forbidden'); $step_id = isset($_POST['step']) ? sanitize_key((string) $_POST['step']) : ''; check_admin_referer('att_hc_refresh_checks_' . $step_id); $session = ATT_HC_Session::current(); if (!$session || $session->is_finished()) wp_die('No active session.'); $step = ATT_HC_Steps::instance()->get($step_id); if (!$step) wp_die('Unknown step.'); @set_time_limit(60); $findings = $step->autocheck($session->data()); try { $session->set_autocheck($step_id, $findings); } catch (ATT_HC_Api_Exception $e) { set_transient('att_hc_api_error', 'Autocheck ran but the result could not be saved to the central server: ' . $e->getMessage(), 60); } wp_safe_redirect(admin_url('tools.php?page=att-site-healthcheck#step-' . rawurlencode($step_id))); exit; } function att_hc_handle_download_report(): void { if (!current_user_can('manage_options')) wp_die('Forbidden'); check_admin_referer('att_hc_download_report'); $session = ATT_HC_Session::current(); if (!$session) wp_die('No session.'); att_hc_stream_report($session, 'md'); } function att_hc_handle_download_html(): void { if (!current_user_can('manage_options')) wp_die('Forbidden'); check_admin_referer('att_hc_download_html'); $session = ATT_HC_Session::current(); if (!$session) wp_die('No session.'); att_hc_stream_report($session, 'html'); } function att_hc_stream_report(ATT_HC_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()); nocache_headers(); if ($format === 'html') { header('Content-Type: text/html; charset=UTF-8'); header('Content-Disposition: attachment; filename="att-hc-report-' . $host . '-' . $stamp . '.html"'); echo att_hc_build_html_report($session); } else { header('Content-Type: text/markdown; charset=UTF-8'); header('Content-Disposition: attachment; filename="att-hc-report-' . $host . '-' . $stamp . '.md"'); echo att_hc_build_markdown_report($session); } exit; } function att_hc_handle_email_report(): void { if (!current_user_can('manage_options')) wp_die('Forbidden'); check_admin_referer('att_hc_email_report'); $session = ATT_HC_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 = att_hc_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=att-site-healthcheck&att_hc_mail=' . ($ok ? '1' : '0'))); exit; }