';
echo '
Site Healthcheck ';
if ($cfg_err = ATT_HC_Api::config_error()) {
echo 'Central history server not usable: ' . esc_html($cfg_err) . '
';
echo '
Add the following to wp-config.php and reload:
';
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 '';
}
if ($err = get_transient('att_hc_api_error')) {
delete_transient('att_hc_api_error');
echo 'Central history server: ' . 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, include_steps: false, limit: 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.
In-progress healthchecks for
Pick up where a previous session left off, or start a fresh one below.
Started Last activity Reporting URL Action
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 '
';
}
function att_hc_print_step_history_assets(ATT_HC_Session $session): void {
$cfg = [
'ajaxUrl' => 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(
'%s ',
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 '
';
foreach ($blocked as $b) {
$esc = $b['step']->escalation();
echo '' . esc_html($b['step']->title()) . ' ';
if ($esc) echo ' — ' . esc_html($esc);
if (!empty($b['state']['notes'])) echo '' . esc_html($b['state']['notes']) . ' ';
echo ' ';
}
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'];
?>
0): ?>
Previous notes for this step ()
blurb()): ?>
sub_items()): ?>
watch_outs()): ?>
escalation())): ?>
render_extra($session->data());
?>
getMessage(), 60);
}
wp_safe_redirect(admin_url('tools.php?page=att-site-healthcheck'));
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
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).
Save settings
Install now
Install from gitea (latest)
✓ 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_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;
}