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:
2026-07-23 12:02:39 +01:00
parent f551b64e2e
commit 631385721f
17 changed files with 1116 additions and 12 deletions

View File

@@ -70,6 +70,7 @@ final class ATT_HC_Session {
'site_url' => $reporting_url,
'wp_version' => get_bloginfo('version'),
'php_version' => PHP_VERSION,
'next_due' => null,
'steps' => [],
];
@@ -180,6 +181,12 @@ final class ATT_HC_Session {
public function php_version(): string { return (string) ($this->data['php_version'] ?? ''); }
public function data(): array { return $this->data; }
/** Scheduled date of the next healthcheck for this site, as 'YYYY-MM-DD', or null. */
public function next_due(): ?string {
$raw = $this->data['next_due'] ?? null;
return is_string($raw) && $raw !== '' ? $raw : null;
}
public function step_state(string $step_id): array {
return $this->data['steps'][$step_id] ?? [
'status' => self::STATUS_NOT_STARTED,
@@ -214,6 +221,40 @@ final class ATT_HC_Session {
update_option(ATT_HC_OPT_SESSION, $this->data, false);
}
/**
* Schedule (or clear, with null) the next healthcheck for this site.
*
* Write-through like every other mutation: the server PUT has to succeed
* before the local cache moves. Allowed on a finished session too — the
* natural moment to decide when to come back is at wrap-up, which may be
* after the report has been generated.
*
* @param string|null $date 'YYYY-MM-DD', or null to clear.
* @throws ATT_HC_Api_Exception
*/
public function set_next_due(?string $date): void {
$date = $date === null ? null : self::sanitise_due_date($date);
ATT_HC_Api::update_healthcheck($this->id(), ['next_due' => $date]);
$this->data['next_due'] = $date;
update_option(ATT_HC_OPT_SESSION, $this->data, false);
}
/**
* Normalise a user-entered date to 'YYYY-MM-DD', or null if it isn't a real
* calendar date in that format.
*
* The round-trip comparison is what makes this strict: createFromFormat
* happily accepts '2026-2-3' and rolls '2026-02-30' forward into March, and
* both re-format to something other than what was typed.
*/
public static function sanitise_due_date(string $raw): ?string {
$raw = trim($raw);
if ($raw === '') return null;
$date = DateTimeImmutable::createFromFormat('!Y-m-d', $raw);
if ($date === false || $date->format('Y-m-d') !== $raw) return null;
return $raw;
}
/**
* Mark the session finished. Server PUT first, then cache update.
*
@@ -294,6 +335,9 @@ final class ATT_HC_Session {
'site_url' => (string) $remote['reporting_url'],
'wp_version' => (string) ($remote['wp_version'] ?? ''),
'php_version' => (string) ($remote['php_version'] ?? ''),
'next_due' => isset($remote['next_due']) && $remote['next_due'] !== null && $remote['next_due'] !== ''
? (string) $remote['next_due']
: null,
'steps' => [],
'autocheck' => [],
];