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.
9.8 KiB
Project Instructions for AI Agents
This file provides instructions and context for AI coding agents working on this project.
Beads Issue Tracker
This project uses bd (beads) for issue tracking. Run bd prime to see full workflow context and commands.
Quick Reference
bd ready # Find available work
bd show <id> # View issue details
bd update <id> --claim # Claim work
bd close <id> # Complete work
Rules
- Use
bdfor ALL task tracking — do NOT use TodoWrite, TaskCreate, or markdown TODO lists - Run
bd primefor detailed command reference and session close protocol - Use
bd rememberfor persistent knowledge — do NOT use MEMORY.md files
Session Completion
When ending a work session, you MUST complete ALL steps below. Work is NOT complete until git push succeeds.
MANDATORY WORKFLOW:
- File issues for remaining work - Create issues for anything that needs follow-up
- Run quality gates (if code changed) - Tests, linters, builds
- Update issue status - Close finished work, update in-progress items
- PUSH TO REMOTE - This is MANDATORY:
git pull --rebase bd dolt push git push git status # MUST show "up to date with origin" - Clean up - Clear stashes, prune remote branches
- Verify - All changes committed AND pushed
- Hand off - Provide context for next session
CRITICAL RULES:
- Work is NOT complete until
git pushsucceeds - NEVER stop before pushing - that leaves work stranded locally
- NEVER say "ready to push when you are" - YOU must push
- If push fails, resolve and retry until it succeeds
Build & Test
There is no build step and no package manager. Plain PHP on both sides; the only
third-party code is a vendored copy of Plugin Update Checker under
vendor/plugin-update-checker/ (do not hand-edit it — see README for the refresh
procedure).
php tests/run.php # everything
php tests/run.php plugin # plugin unit tests only (runs on PHP 7.4+)
php tests/run.php server # server integration tests only (needs PHP 8.1+)
The suite is a hand-rolled harness in tests/lib/ — no PHPUnit, no composer,
matching the rest of the repo. Two kinds of test:
tests/plugin_test.php— unit tests for the pure static helpers onATT_HC_Session. That class calls no WP functions at load time, so definingABSPATHis enough to require it; anything needing a real WordPress runtime is out of scope here.tests/server_test.php— end-to-end over real HTTP. The runner copiesserver/to a temp dir, gives it a throwaway SQLite DB and config, and startsphp -Sagainst it. A developer's realserver/config.php(gitignored, may point at live MySQL) is never read or touched. Skips itself with a clear message ifpdo_sqliteis missing.
Exit code is non-zero on failure, so it works as a pre-push gate. Add a test with
test('name', function () { ... }) and the assert_* helpers in
tests/lib/harness.php.
Also lint the files you touched, against the right PHP version for that half of the repo (see Conventions):
# Plugin — must parse on PHP 7.4
find . -path ./vendor -prune -o -path ./server -prune -o -name '*.php' -print \
| xargs -n1 php7.4 -l # fall back to `php -l` if 7.4 isn't installed
# Server — PHP 8.1+
find server -name '*.php' -print0 | xargs -0 -n1 php -l
Run the history server locally:
cp server/config.php.example server/config.php # set api_key + db_dsn (sqlite is easiest locally)
php -S 127.0.0.1:8765 server/dev-router.php
curl 127.0.0.1:8765/ # → {"ok":true,...}
Use dev-router.php, not -t server/public. Healthcheck ids are dotted
uniqid() values and PHP's built-in server treats dotted path segments as static
file requests, so the plain docroot form 404s on most real URLs.
The plugin has to be exercised inside a WordPress install — symlink the repo into
wp-content/plugins/att-site-healthcheck and use Tools → Site Healthcheck. It
refuses to run until ATT_HC_API_URL / ATT_HC_API_KEY are configured (wp-config
constants win over the settings-screen options).
Server deploy is an rsync of server/ — see server/DEPLOY.md. Migrations run
automatically on the first request after deploy.
Architecture Overview
Two independent deployables live in this one repo, and they must not leak into each other:
- The WordPress plugin — repo root,
includes/,vendor/. Ships towp-content/plugins/. Installed per engagement and uninstalled at the end. - The central history server (
server/) — a standalone PHP app deployed to a VPS..gitattributesmarksserver/,.beads/,.claude/,AGENTS.md,CLAUDE.md, andsteps.mdasexport-ignore, so Gitea'sarchive/main.zip(which is the plugin release artifact) excludes them. Any new non-plugin file at the repo root needs anexport-ignoreline.
Plugin
att-site-healthcheck.php— header, constants, requires, step discovery onplugins_loaded, PUC update checker wiring.includes/steps/<order>-<slug>.php— one file per healthcheck step, eachreturning anATT_HC_Stepsubclass instance. Adding a step is dropping a file; reordering is renaming the numeric prefix (loaded innatsortorder); removing is deleting the file.ATT_HC_Stepsis the singleton registry and applies theatt_hc_stepsfilter after discovery.ATT_HC_Session— the server is the source of truth. Theatt_hc_sessionWP option is only a cache of the active session. Every write is write-through: API call first, local cache updated only on success, so the cache can never drift. Cross-engagement history (prior notes, previous-session diff, step counts) is fetched live.ATT_HC_Api—wp_remote_requestclient; every method throwsATT_HC_Api_Exception. Refuses non-https://endpoints unless loopback.includes/admin-page.php— the whole UI (renderers +admin_post_*handlers).includes/report.php— Markdown report builder. Reports are never stored server-side; the technician downloads them.includes/recovery-bootstrap.php/recovery-installer.php— step 0 detects and can install the companionwp-site-recoveryplugin (detected by plugin Name + Author, not folder slug).
Server (server/)
Front controller → src/bootstrap.php (hand-rolled PSR-4 autoloader for
AttHc\Server\, error/exception handlers, config.php load) → src/routes.php
→ Router → Controllers/. Store holds all SQL; Db is a PDO singleton that
runs Migrations::ensureRan() on first connection.
- API routes take
Authorization: Bearer <api_key>(Auth::require,hash_equals).GET /is deliberately public — the plugin pings it to check reachability. /dashboard*isrequiresAuth: falseat the router level because it does its own auth:?key=<api_key>once, then a PHP session cookie, plus a CSRF token on POSTs.- Storage is PDO against either MySQL 8.0+ or SQLite —
config.php.exampledefaults to MySQL becausepdo_sqliteis missing on some Ubuntu + Sury PHP combinations. Keepmigrations/*.sqlportable across both (VARCHAR(N),BIGINT, noAUTO_INCREMENT); driver-specific DDL needs dispatch inMigrations.php.
Conventions & Patterns
PHP version split — the easiest mistake to make here
- Plugin code (root +
includes/) must parse on PHP 7.4. Nostr_starts_with/str_contains/str_ends_with, no named arguments, nomatch, no nullsafe?->, no constructor promotion, no enums. This has been broken by drift before (commit3c64dd8) — lint against 7.4, don't just eyeball it. - Server code (
server/) targets PHP 8.1+ and freely usesdeclare(strict_types=1), typed properties,never, named arguments, and promotion.
Plugin
- Guard every file with
if (!defined('ABSPATH')) exit;. - Admin actions go through
admin_post_*handlers withcurrent_user_can('manage_options')wp_nonce_field()/check_admin_referer(), and escape on output (esc_html/esc_attr).
- A step's
id()is a stable contract — it is the key stored in session and server data. Rename files freely, never rename an id once shipped. - Optional step behaviour hangs off
ATT_HC_Step:autocheck()for findings,render_extra()+handle_action()for step-specific UI/POSTs. - API failures surface as admin notices. History/diff lookups degrade silently (nice-to-have); writes must not.
Server
- Every file:
declare(strict_types=1);and namespaceAttHc\Server\.... - All SQL lives in
Store, always via prepared statements. - Request input goes through
Validate::*; responses throughHttp::json/Http::error({"error":…,"code":…}). - No composer. If you think you need a dependency, vendor it or don't.
server/config.phpis deployment-specific and gitignored — never commit it, and don't echo secrets into responses or logs.
Repo / release
- Remote is Gitea (
git.h12e.com/steve/wp-healthcheck), not GitHub —ghand PR workflows don't apply. Work lands onmain. - Release = bump
Version:in the plugin header andATT_HC_VERSIONandversion+last_updatedinupdates.json, then pushmain. PUC pollsupdates.jsonand installsarchive/main.zip, so whatever is onmainis what ships. Built zips are gitignored. steps.mdis the human technician guide the step files are derived from; keep the two in step when step content changes.- Known open security issues are tracked in beads —
hc-gp3(API key exposure),hc-adg(unpinned recovery-plugin install),hc-ufl(no rate limiting / unbounded error log). Check them before touching auth, the settings screen, or the installer.