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.
The Build & Test, Architecture Overview, and Conventions & Patterns sections
were still the init template's "_Add your ... here_" stubs. Filled them in
from the code:
- No build/test tooling exists; documents the php -l quality gate and the
dev-router.php requirement for running the server locally.
- Describes the two-deployable split (WP plugin vs server/) and the
.gitattributes export-ignore rule that keeps them apart in release zips.
- Records the PHP 7.4 (plugin) / 8.1+ (server) version split that already
drifted once in 3c64dd8.
- Notes Gitea (not GitHub) as the remote, the three-file release bump, and
the open security beads to check before touching auth or the installer.
The managed beads integration block is unchanged.
Adds a Reopen button on the session-detail view for finished sessions.
POSTs to /dashboard/healthchecks/{id}/reopen with a per-session CSRF
token; on success clears finished_at and redirects back to the session.
Also tightens Controllers/Healthchecks::update so a PUT with no
finished_at key is a no-op instead of silently setting it to NULL —
otherwise an empty PUT would reopen any completed healthcheck.
After reopening, the WP-side Session::resume($id) flow pulls the now-
active session back into an editable local session on the client site.
Reverts the earlier Apache-2.0 change in favour of a proprietary licence
matching the other internal ATT plugins (att-consent). Grants use only to
authorised sites; all other rights reserved. MIT carve-out preserved for
vendored PUC.
Three issues from the 2026-07-16 security review:
- hc-gp3 (P2): API key echoed in settings input + plaintext in wp_options
- hc-adg (P2): recovery installer pulls unpinned 'latest' without signature
- hc-ufl (P3): no rate limiting on API or /dashboard, unrotated error_log
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Vendors YahnisElsts/plugin-update-checker v5.7 under vendor/ and wires it
to poll updates.json on the repo main branch. WP surfaces new versions
via Dashboard → Updates on its normal twice-daily cadence.
PUC is vendored (not a submodule) so Gitea's archive/main.zip — the
download target — includes it. Folder rename on install is handled by
PUC's built-in upgrader_source_selection filter.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Slot two new steps between performance (Step 7) and security (Step 8):
- 72-analytics.php: sniffs the homepage for GA4 (G-), GTM, and legacy UA
measurement IDs plus known loader URLs (gtag.js, gtm.js, analytics.js,
ga.js) and detects common analytics/tag plugins. Warns if only UA is
still in use.
- 74-search-console.php: looks for google-site-verification meta tags on
the homepage, probes for a reachable sitemap (wp-sitemap.xml, then
sitemap_index.xml, then sitemap.xml), parses robots.txt for a
Googlebot/* Disallow: /, flags the WP "Discourage search engines"
setting when on, and notes whether Site Kit is active.
Titles use the "Step —" (unnumbered) convention already used by the
email and handover steps so the existing numbered steps don't shift.
steps.md updated to match.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Some managed hosts (WPE and various resellers, plus some clients' own ops
teams) don't let us edit wp-config.php. Add a settings-screen alternative
so the plugin can be configured without touching filesystem constants.
Storage + resolution:
- Two new options: att_hc_api_url and att_hc_api_key, autoload=false on
the key so it isn't loaded on every request.
- ATT_HC_Api::url() and ATT_HC_Api::key() are the single source of truth
now — they return the wp-config constant when defined+non-empty, else
the option, else ''. Everything else (request(), config_error(),
is_configured(), the admin config-error notice) uses these accessors.
- url_from_constant() / key_from_constant() drive per-field locking on
the settings page and are also checked by the save handler so a
constant-locked field can't be overridden by a crafted POST.
UI:
- New "Central history server" card at the top of Tools → Site
Healthcheck → Settings with URL (type=url) and API key (type=password)
inputs. When a constant is defined the field is disabled with a
"Set via <constant> constant" hint.
- Separate form action/nonce (att_hc_save_api_settings) so it doesn't
tangle with the existing Gitea recovery save.
- The blocking config-error notice on the main page now offers an
"Open settings" button alongside the wp-config.php snippet.
Verified with an 18-assertion test suite covering no-config, options-
only, http-blocked-with-clear-message, loopback-http-allowed, and
constant-wins-over-option. Both PHP 8.3 and PHP 7.4 parse cleanly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add a final "Notes for next time" step so the tech finishing today can flag
pending issues, watch-fors, and outstanding client decisions for whoever
picks up the next healthcheck on the same site.
On ATT_HC_Session::start() for a given site_key, the server's step history
for the handover step is queried (limit 1, excluding the just-created
session). If a prior session left handover notes, they're written into
the new session's "Before You Start" notes prefixed with the prior
session's date ("From previous session (YYYY-MM-DD):") so the carry-over
is obvious. The tech can edit/clear them as normal step notes from there.
- includes/steps/125-handover.php — new step (id=handover) using the
standard notes field. No server schema or API change; it's just another
step row in step_updates, surfaced like any other.
- ATT_HC_Session::seed_before_notes_from_prior_handover() — best-effort,
silent degrade on API failure. The session is already registered on
the server before this runs, so a failed seed never blocks start.
- No seed on resume() — resuming an existing session would clobber
whatever the tech had already typed.
Verified end-to-end against the live MySQL server: handover-test-XXXX
flow shows carry-over with date prefix; no-handover-XXXX flow confirms
no false-positive seed for a fresh site_key. Test rows purged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The central history server work introduced four PHP 8.0+ syntax sites in
the plugin codebase. The main plugin file's "Requires PHP: 7.4" header
was already there; the code had silently drifted past that bound.
- includes/class-att-hc-session.php:244 — str_starts_with($host, 'www.')
→ substr($host, 0, 4) === 'www.'.
- includes/class-att-hc-api.php:52 — self::request(…, requires_auth: false)
→ positional false. Same default semantics, same callee signature.
- includes/admin-page.php:165 and class-att-hc-session.php:126 —
list_healthchecks(…, include_steps: …, limit: …) → positional. Same
values, no semantic change.
Verified by linting all 16 plugin files against PHP 7.4.33 — no syntax
errors, no residual PHP 8+ patterns (str_starts_with/contains/ends_with,
nullsafe, enum, readonly, mixed/never, constructor promotion, match).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
Demonstrates the drop-in pattern: adding a step with a form is a single
new file under includes/steps/, plus one small extension to the base
abstraction.
WPH_Step gains:
- render_extra($session_state) — emit extra HTML inside the step card
- handle_action($name, $input) — handle a step-specific POST and
return a finding to record
New generic admin-post handler wph_step_action routes form submissions
to the matching step's handle_action() with nonce verification.
The email step (includes/steps/115-email.php):
- Detects 7 known SMTP plugins, surfaces default From address
- Renders a To: input prefilled with the current admin's email
- Sends via wp_mail() with wp_mail_failed capture so failures show the
underlying error
- Records the outcome as a 'last_send' finding so it appears on the
step card, in the summary, and in the downloadable report
Infrastructure:
- WPH_Step::autocheck($session_state) returns an array of findings
shaped {id, level: ok/warn/bad/info, label, value, detail}.
- WPH_Session stores results keyed by step id (persisted in the option-
backed session).
- WPH_Step::has_autocheck() reflection check so the UI only renders the
panel for steps that implement automation.
- 'Run checks' / 'Refresh' button per step, admin-post handler runs
autocheck() and stashes the result on the session.
- Findings rendered as a coloured table on the step card; included
verbatim in the Markdown report with status icons.
Step automations implemented:
- Step 1 (Backup): detection of 11 known backup plugins by slug;
active/inactive state; UpdraftPlus last-backup timestamp.
- Step 2 (Environment): PHP version + EOL, WP version vs latest, disk
usage, wp-config flags, file perms on wp-config/wp-content/uploads,
error-log sizes.
- Step 4 (Plugins): WP.org API enrichment with 24h transient cache —
last_updated, active_installs, abandonment flag, removed-from-repo
flag, update-available count. Summary line at the top.
- Step 8 (Security): SSL cert expiry via stream_socket_client +
openssl_x509_parse, administrator audit, xmlrpc reachability, login
URL hardening detection.
- Step 9 (Database): spam comments, post revisions, autoload size (WP
6.6+ value handling), top 3 largest tables.
- Step 11 (Small fixes): deactivated-but-installed plugin list,
homepage alt-text scan.
Smoke-tested on testsite — all six steps return findings with
correctly-classified levels. Report regenerated with automated findings
section.
Plugin skeleton, drop-in step registry, option-backed session, single-page
checklist UI, downloadable Markdown report. Steps live as one file each
under includes/steps/ — adding/removing one is a single file change.
Step IDs are stable strings so renaming files preserves session data.
Architecture (hc-5ix.3): WPH_Steps singleton globs includes/steps/*.php,
natsort-orders by filename, requires each file (which returns a WPH_Step
instance), then applies a 'wph_steps' filter so installs can drop steps.
Session (hc-5ix.4): option-backed (per decision — plugin is installed
per-engagement, so DB-resident history would be lost on uninstall).
Single in-progress session per site; finished sessions render a report
that the user downloads/copies.
Recovery bootstrap (hc-5ix.1): detects whether wp-site-recovery is
installed + active, surfaces state on the start panel and in every
active session. Manual install for now; private update channel deferred
to hc-5ix.27.
Smoke-tested on testsite: registry discovery (13 steps in correct order),
start → update_step → progress count → finish → 8KB Markdown report →
discard cycle.