Next healthcheck due date, plus a test suite (hc-nkq)
Lets a technician record when a site should next be looked at, and surfaces that in the dashboard so it can be used for planning. Server: - migration 0002 adds healthchecks.next_due, a VARCHAR(10) 'YYYY-MM-DD' calendar date rather than a timestamp — it's a diary date with no time-of-day, and a timestamp would render as the wrong day off-server. - Kept per-session rather than on a sites table, so the history of what was scheduled when is preserved. A site's current next-due is the value on its most recent session: if the latest visit scheduled nothing, the site reads as unscheduled rather than showing the just-completed visit as overdue. - Validate::optionalDate() round-trips through createFromFormat, which rejects both '2026-2-3' and '2026-02-30' (silently rolled to March 2nd). - next_due follows the same missing-vs-null PUT contract as finished_at, and lives in its own Store::setNextDue() so finishing or reopening a session never disturbs the date and vice versa. - Dashboard shows it on the site list, the per-site session table and the session detail, flagged overdue / today / soon (within a fortnight). Plugin: - Date control on both the active and finished panels — the moment you know when to return is often wrap-up, after the report is generated. - Write-through like every other mutation. Bad input is rejected with a notice rather than silently clearing an existing date. - Included in both the Markdown and HTML reports. Tests (new — tests/, export-ignored from the plugin zip): - Hand-rolled harness, no composer/PHPUnit, in keeping with the repo. - 42 tests: plugin-side date parsing units, plus server integration tests driven over real HTTP against a temp `php -S` instance with a throwaway SQLite DB, so a real server/config.php is never touched. - Mutation-checked: dropping the array_key_exists guard on PUT fails three tests, as intended.
This commit is contained in:
@@ -7,6 +7,7 @@ add_action('admin_post_att_hc_resume', 'att_hc_handle_resume');
|
||||
add_action('wp_ajax_att_hc_step_history', 'att_hc_handle_step_history_ajax');
|
||||
add_action('admin_post_att_hc_save_step', 'att_hc_handle_save_step');
|
||||
add_action('admin_post_att_hc_finish', 'att_hc_handle_finish');
|
||||
add_action('admin_post_att_hc_save_next_due', 'att_hc_handle_save_next_due');
|
||||
add_action('admin_post_att_hc_discard', 'att_hc_handle_discard');
|
||||
add_action('admin_post_att_hc_download_report', 'att_hc_handle_download_report');
|
||||
add_action('admin_post_att_hc_refresh_checks', 'att_hc_handle_refresh_checks');
|
||||
@@ -53,6 +54,9 @@ function att_hc_inline_css(): string {
|
||||
.att-hc-status-skipped { background:#fff3cd; color:#856404; }
|
||||
.att-hc-status-blocked { background:#fbeae8; color:#721c24; }
|
||||
.att-hc-status-n_a { background:#e2e3e5; color:#41464b; }
|
||||
.att-hc-next-due { margin:.5rem 0 .75rem; padding:.5rem .75rem; background:#f6f7f7; border:1px solid #dcdcde; border-radius:4px; }
|
||||
.att-hc-next-due form { display:flex; flex-wrap:wrap; align-items:center; gap:.5rem; }
|
||||
.att-hc-next-due .description { flex-basis:100%; }
|
||||
.att-hc-step { padding:1rem 1.25rem; border:1px solid #dcdcde; border-radius:6px; margin-bottom:.75rem; background:#fff; }
|
||||
.att-hc-step header { display:flex; justify-content:space-between; align-items:center; gap:1rem; margin-bottom:.5rem; }
|
||||
.att-hc-step header h2 { margin:0; font-size:1.1rem; }
|
||||
@@ -133,6 +137,12 @@ function att_hc_render_admin_page(): void {
|
||||
echo '<div class="notice notice-error"><p><strong>Central history server:</strong> ' . esc_html($err) . '</p></div>';
|
||||
}
|
||||
|
||||
// Local (non-server) problems — bad input and the like.
|
||||
if ($err = get_transient('att_hc_error')) {
|
||||
delete_transient('att_hc_error');
|
||||
echo '<div class="notice notice-error is-dismissible"><p>' . esc_html($err) . '</p></div>';
|
||||
}
|
||||
|
||||
if (!$session) {
|
||||
att_hc_render_start_panel();
|
||||
echo '</div>';
|
||||
@@ -255,6 +265,7 @@ function att_hc_render_active_session(ATT_HC_Session $session): void {
|
||||
<span class="att-hc-progress"><?php echo (int) $progress['done']; ?> / <?php echo (int) $progress['total']; ?> steps</span>
|
||||
</p>
|
||||
<p>WP <code><?php echo esc_html($session->wp_version()); ?></code> · PHP <code><?php echo esc_html($session->php_version()); ?></code> · Site <code><?php echo esc_html($session->site_url()); ?></code></p>
|
||||
<?php att_hc_render_next_due_form($session); ?>
|
||||
<div class="att-hc-actions">
|
||||
<form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>" onsubmit="return confirm('Mark this healthcheck as finished?');" style="display:inline">
|
||||
<?php wp_nonce_field('att_hc_finish'); ?>
|
||||
@@ -295,6 +306,30 @@ function att_hc_render_active_session(ATT_HC_Session $session): void {
|
||||
echo '</div></div>';
|
||||
}
|
||||
|
||||
/**
|
||||
* "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();
|
||||
?>
|
||||
<div class="att-hc-next-due">
|
||||
<form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>">
|
||||
<?php wp_nonce_field('att_hc_save_next_due'); ?>
|
||||
<input type="hidden" name="action" value="att_hc_save_next_due">
|
||||
<label for="att-hc-next-due"><strong>Next healthcheck due:</strong></label>
|
||||
<input type="date" id="att-hc-next-due" name="next_due" value="<?php echo esc_attr((string) $due); ?>">
|
||||
<button class="button">Save date</button>
|
||||
<?php if ($due !== null) : ?>
|
||||
<button class="button button-link" name="next_due_clear" value="1">Clear</button>
|
||||
<?php endif; ?>
|
||||
<span class="description">Shown against this site on the healthcheck dashboard.</span>
|
||||
</form>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
function att_hc_print_step_history_assets(ATT_HC_Session $session): void {
|
||||
$cfg = [
|
||||
'ajaxUrl' => admin_url('admin-ajax.php'),
|
||||
@@ -546,6 +581,7 @@ function att_hc_render_finished_panel(ATT_HC_Session $session): void {
|
||||
<div class="att-hc-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>
|
||||
<?php att_hc_render_next_due_form($session); ?>
|
||||
<div class="att-hc-actions">
|
||||
<form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>" style="display:inline">
|
||||
<?php wp_nonce_field('att_hc_download_report'); ?>
|
||||
@@ -601,6 +637,41 @@ function att_hc_handle_start(): void {
|
||||
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)) {
|
||||
|
||||
Reference in New Issue
Block a user