';
}
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.
In-progress healthchecks for
Pick up where a previous session left off, or start a fresh one below.
';
}
/**
* "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();
?>
0): ?>
Previous notes for this step ()
blurb()): ?>
sub_items()): ?>
watch_outs()): ?>
Watch out for:
escalation()): ?>
>
render_extra($session->data());
?>
Healthcheck finished
Started started_at())); ?> · Finished finished_at())); ?>
Begins a fresh session for site_key()); ?>. This finished report stays on the history server.
Report preview
getMessage(), 60);
}
wp_safe_redirect(admin_url('tools.php?page=att-site-healthcheck'));
exit;
}
/**
* Start a fresh healthcheck straight from the finished panel, without making the
* technician Discard first (Discard reads as destructive, though it only clears
* the local cache). Only acts on a finished session; the finished record stays
* on the history server.
*/
function att_hc_handle_start_next(): void {
if (!current_user_can('manage_options')) wp_die('Forbidden');
check_admin_referer('att_hc_start_next');
$current = ATT_HC_Session::current();
// Guard: only from a finished session. If an active one is somehow current,
// don't clobber it — just send the technician back to it.
if ($current && !$current->is_finished()) {
wp_safe_redirect(admin_url('tools.php?page=att-site-healthcheck'));
exit;
}
// Keep the engagement's site_key so history links up and the just-finished
// session's handover notes seed the new "before" notes (see start()).
$site_key = isset($_POST['site_key']) && $_POST['site_key'] !== ''
? sanitize_text_field(wp_unslash((string) $_POST['site_key']))
: ($current ? $current->site_key() : null);
// Drop the finished session from the local cache so start() creates a new one.
// The finished record remains on the server.
ATT_HC_Session::discard();
try {
ATT_HC_Session::start(get_current_user_id(), $site_key);
} catch (ATT_HC_Api_Exception $e) {
set_transient('att_hc_api_error', 'Could not start a new healthcheck: ' . $e->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;
}
/**
* AJAX twin of att_hc_handle_save_step(). Same validation and write-through,
* but returns JSON instead of redirecting, so the reporting page can save a
* step in place (on status change / notes blur) without a full reload.
*
* The non-AJAX form POST above is kept as the no-JS fallback.
*/
function att_hc_handle_save_step_ajax(): void {
if (!current_user_can('manage_options')) wp_send_json_error('Forbidden', 403);
if (!check_ajax_referer('att_hc_save_step_ajax', 'nonce', false)) {
wp_send_json_error('Bad nonce — reload the page and try again.', 403);
}
$step_id = isset($_POST['step']) ? sanitize_key((string) $_POST['step']) : '';
if ($step_id === '') wp_send_json_error('Missing step id', 400);
$session = ATT_HC_Session::current();
if (!$session || $session->is_finished()) wp_send_json_error('No active session', 409);
if (!ATT_HC_Steps::instance()->get($step_id)) wp_send_json_error('Unknown step', 400);
$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 {
// update_step() re-validates the status and falls back to NOT_STARTED
// for anything unexpected, so we report back whatever was actually stored.
$session->update_step($step_id, $status, $notes);
} catch (ATT_HC_Api_Exception $e) {
wp_send_json_error('Not saved — the central server rejected the write: ' . $e->getMessage(), 502);
}
$state = $session->step_state($step_id);
wp_send_json_success([
'step' => $step_id,
'status' => (string) $state['status'],
'status_label' => str_replace('_', ' ', (string) $state['status']),
'updated_at' => (int) $state['updated_at'],
]);
}
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
No automated checks have been run for this step yet.
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.
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).
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()) . '