Files
wp-healthcheck/server/src/Controllers/Steps.php
Steve Hanlon 1782e52504 Central history server + plugin write-through sync (epic hc-0p1)
Adds a PHP/SQLite history server in server/ and refactors the plugin to
write every session change through it. Healthcheck history now survives
plugin uninstall and groups across dev + live URLs for the same engagement
via an editable site_key (defaults to the normalised host).

Server (server/):
- Front controller + hand-rolled autoloader, no framework, no composer
- SQLite default DSN; swap to MySQL by changing config.php
- Schema: healthchecks (PK id, UNIQUE (site_key, started_at)) + step_updates
  (PK (healthcheck_id, step_id)) + request_log; auto-migration runner
- 8 endpoints: POST/GET/PUT healthchecks, PUT/GET step rows, GET step history
  with exclude_id, GET /sites (recent), GET /step-counts (badge data)
- Bearer auth via hash_equals; HTTPS expected (plugin enforces client-side)
- DEPLOY.md with Apache/nginx vhosts, Let's Encrypt, SQLite backup cron,
  and the /home/www/ perm gotcha
- dev-router.php works around PHP -S 405-ing dotted uniqid paths

Plugin:
- ATT_HC_Api HTTP client reads ATT_HC_API_URL/ATT_HC_API_KEY constants
  from wp-config.php; refuses non-HTTPS with a loopback dev exception
- ATT_HC_Session is now write-through: every start/update_step/finish/
  set_autocheck POSTs or PUTs to the server first, then updates the local
  WP option cache. No drift possible — failures throw ATT_HC_Api_Exception
- previous() now reads from /healthchecks?include=steps and reconstructs;
  the old att_hc_previous_session local option is gone
- ATT_HC_Session::resume(id) hydrates a server session into the local cache
- Start screen: editable site_key (defaults to normalise_site_url()),
  datalist of recent engagements, table of in-progress sessions for the
  chosen key with Resume buttons. Double-click guard on start + resume
  handlers short-circuits if a session is already active
- Per-step <details> disclosure shows "Previous notes (N)" badge from
  /step-counts; lazy-loads detail rows on first expand via admin-ajax,
  caches via data-loaded, resets on error so user can retry
- All admin handlers catch ATT_HC_Api_Exception and surface via
  att_hc_api_error transient → admin notice
- Hard config-error gate at the top of the admin page blocks the UI when
  ATT_HC_API_URL/ATT_HC_API_KEY are missing or malformed

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-29 12:43:24 +01:00

55 lines
2.1 KiB
PHP

<?php
declare(strict_types=1);
namespace AttHc\Server\Controllers;
use AttHc\Server\Http;
use AttHc\Server\Store;
use AttHc\Server\Validate;
final class Steps {
public static function upsert(array $params): void {
$healthcheckId = $params['id'];
$stepId = $params['step_id'];
if (Store::getHealthcheck($healthcheckId) === null) {
Http::error(404, 'not_found', 'no healthcheck with that id');
return;
}
$body = Http::readJsonBody();
$row = [
'status' => Validate::status(Validate::requireString($body, 'status', 32)),
'notes' => $body['notes'] ?? '',
'reporting_url' => Validate::requireString($body, 'reporting_url', 512),
'autocheck' => isset($body['autocheck']) && is_array($body['autocheck']) ? $body['autocheck'] : null,
];
if (!is_string($row['notes'])) Validate::fail('notes must be a string');
Store::upsertStep($healthcheckId, $stepId, $row);
Http::json(200, ['ok' => true]);
}
public static function history(array $params): void {
$stepId = $params['step_id'];
$siteKey = $_GET['site_key'] ?? '';
if (!is_string($siteKey) || $siteKey === '') {
Http::error(422, 'invalid', 'site_key query param is required');
return;
}
$limit = max(1, min(50, (int) ($_GET['limit'] ?? 5)));
$excludeId = isset($_GET['exclude_id']) && is_string($_GET['exclude_id']) ? $_GET['exclude_id'] : null;
Http::json(200, ['history' => Store::stepHistory($stepId, $siteKey, $limit, $excludeId)]);
}
public static function counts(): void {
$siteKey = $_GET['site_key'] ?? '';
if (!is_string($siteKey) || $siteKey === '') {
Http::error(422, 'invalid', 'site_key query param is required');
return;
}
$excludeId = isset($_GET['exclude_id']) && is_string($_GET['exclude_id']) ? $_GET['exclude_id'] : null;
Http::json(200, ['counts' => Store::stepCountsForSite($siteKey, $excludeId)]);
}
}