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:
@@ -11,8 +11,8 @@ final class Store {
|
||||
public static function insertHealthcheck(array $row): void {
|
||||
$now = time();
|
||||
$stmt = Db::pdo()->prepare('INSERT INTO healthchecks
|
||||
(id, site_key, started_at, finished_at, technician_id, reporting_url, wp_version, php_version, created_at, updated_at)
|
||||
VALUES (:id, :site_key, :started_at, NULL, :technician_id, :reporting_url, :wp_version, :php_version, :created_at, :updated_at)');
|
||||
(id, site_key, started_at, finished_at, technician_id, reporting_url, wp_version, php_version, next_due, created_at, updated_at)
|
||||
VALUES (:id, :site_key, :started_at, NULL, :technician_id, :reporting_url, :wp_version, :php_version, :next_due, :created_at, :updated_at)');
|
||||
$stmt->execute([
|
||||
':id' => $row['id'],
|
||||
':site_key' => $row['site_key'],
|
||||
@@ -21,6 +21,7 @@ final class Store {
|
||||
':reporting_url' => $row['reporting_url'],
|
||||
':wp_version' => $row['wp_version'] ?? null,
|
||||
':php_version' => $row['php_version'] ?? null,
|
||||
':next_due' => $row['next_due'] ?? null,
|
||||
':created_at' => $now,
|
||||
':updated_at' => $now,
|
||||
]);
|
||||
@@ -43,6 +44,22 @@ final class Store {
|
||||
return $stmt->rowCount() > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set (or clear, with null) the next-due date on one healthcheck.
|
||||
*
|
||||
* Deliberately separate from updateHealthcheck() so that finishing or
|
||||
* reopening a session never touches the scheduled date, and vice versa.
|
||||
*/
|
||||
public static function setNextDue(string $id, ?string $nextDue): bool {
|
||||
$stmt = Db::pdo()->prepare('UPDATE healthchecks SET next_due = :next_due, updated_at = :updated_at WHERE id = :id');
|
||||
$stmt->execute([
|
||||
':id' => $id,
|
||||
':next_due' => $nextDue,
|
||||
':updated_at' => time(),
|
||||
]);
|
||||
return $stmt->rowCount() > 0;
|
||||
}
|
||||
|
||||
public static function upsertStep(string $healthcheckId, string $stepId, array $row): void {
|
||||
$pdo = Db::pdo();
|
||||
$driver = $pdo->getAttribute(\PDO::ATTR_DRIVER_NAME);
|
||||
@@ -180,7 +197,12 @@ final class Store {
|
||||
/**
|
||||
* Summary of all sites for the dashboard: one row per site_key,
|
||||
* ordered by most-recently-active first.
|
||||
* @return array<int, array{site_key:string, total:int, finished:int, last_started_at:int, last_finished_at:int|null, last_reporting_url:string}>
|
||||
* `next_due` is taken from the most recent session, not from the most recent
|
||||
* session that happens to have one set. If the latest visit didn't schedule a
|
||||
* return, the site genuinely has nothing scheduled — carrying an older date
|
||||
* forward would show a visit as overdue immediately after it was completed.
|
||||
*
|
||||
* @return array<int, array{site_key:string, total:int, finished:int, last_started_at:int, last_finished_at:int|null, last_reporting_url:string, next_due:string|null}>
|
||||
*/
|
||||
public static function allSitesSummary(): array {
|
||||
$stmt = Db::pdo()->query(
|
||||
@@ -198,7 +220,7 @@ final class Store {
|
||||
$out = [];
|
||||
foreach ($rows as $r) {
|
||||
$detail = Db::pdo()->prepare(
|
||||
'SELECT reporting_url, finished_at FROM healthchecks
|
||||
'SELECT reporting_url, finished_at, next_due FROM healthchecks
|
||||
WHERE site_key = ? ORDER BY started_at DESC LIMIT 1'
|
||||
);
|
||||
$detail->execute([$r['site_key']]);
|
||||
@@ -210,6 +232,9 @@ final class Store {
|
||||
'last_started_at' => (int) $r['last_started_at'],
|
||||
'last_finished_at' => isset($last['finished_at']) && $last['finished_at'] !== null ? (int) $last['finished_at'] : null,
|
||||
'last_reporting_url' => (string) ($last['reporting_url'] ?? ''),
|
||||
'next_due' => isset($last['next_due']) && $last['next_due'] !== null && $last['next_due'] !== ''
|
||||
? (string) $last['next_due']
|
||||
: null,
|
||||
];
|
||||
}
|
||||
return $out;
|
||||
|
||||
Reference in New Issue
Block a user