Files
wp-healthcheck/CLAUDE.md
Steve Hanlon 631385721f 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.
2026-07-23 12:02:39 +01:00

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 bd for ALL task tracking — do NOT use TodoWrite, TaskCreate, or markdown TODO lists
  • Run bd prime for detailed command reference and session close protocol
  • Use bd remember for 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:

  1. File issues for remaining work - Create issues for anything that needs follow-up
  2. Run quality gates (if code changed) - Tests, linters, builds
  3. Update issue status - Close finished work, update in-progress items
  4. PUSH TO REMOTE - This is MANDATORY:
    git pull --rebase
    bd dolt push
    git push
    git status  # MUST show "up to date with origin"
    
  5. Clean up - Clear stashes, prune remote branches
  6. Verify - All changes committed AND pushed
  7. Hand off - Provide context for next session

CRITICAL RULES:

  • Work is NOT complete until git push succeeds
  • 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 on ATT_HC_Session. That class calls no WP functions at load time, so defining ABSPATH is enough to require it; anything needing a real WordPress runtime is out of scope here.
  • tests/server_test.phpend-to-end over real HTTP. The runner copies server/ to a temp dir, gives it a throwaway SQLite DB and config, and starts php -S against it. A developer's real server/config.php (gitignored, may point at live MySQL) is never read or touched. Skips itself with a clear message if pdo_sqlite is 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:

  1. The WordPress plugin — repo root, includes/, vendor/. Ships to wp-content/plugins/. Installed per engagement and uninstalled at the end.
  2. The central history server (server/) — a standalone PHP app deployed to a VPS. .gitattributes marks server/, .beads/, .claude/, AGENTS.md, CLAUDE.md, and steps.md as export-ignore, so Gitea's archive/main.zip (which is the plugin release artifact) excludes them. Any new non-plugin file at the repo root needs an export-ignore line.

Plugin

  • att-site-healthcheck.php — header, constants, requires, step discovery on plugins_loaded, PUC update checker wiring.
  • includes/steps/<order>-<slug>.php — one file per healthcheck step, each returning an ATT_HC_Step subclass instance. Adding a step is dropping a file; reordering is renaming the numeric prefix (loaded in natsort order); removing is deleting the file. ATT_HC_Steps is the singleton registry and applies the att_hc_steps filter after discovery.
  • ATT_HC_Sessionthe server is the source of truth. The att_hc_session WP 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_Apiwp_remote_request client; every method throws ATT_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 companion wp-site-recovery plugin (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.phpRouterControllers/. 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* is requiresAuth: false at 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 SQLiteconfig.php.example defaults to MySQL because pdo_sqlite is missing on some Ubuntu + Sury PHP combinations. Keep migrations/*.sql portable across both (VARCHAR(N), BIGINT, no AUTO_INCREMENT); driver-specific DDL needs dispatch in Migrations.php.

Conventions & Patterns

PHP version split — the easiest mistake to make here

  • Plugin code (root + includes/) must parse on PHP 7.4. No str_starts_with /str_contains/str_ends_with, no named arguments, no match, no nullsafe ?->, no constructor promotion, no enums. This has been broken by drift before (commit 3c64dd8) — lint against 7.4, don't just eyeball it.
  • Server code (server/) targets PHP 8.1+ and freely uses declare(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 with current_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 namespace AttHc\Server\....
  • All SQL lives in Store, always via prepared statements.
  • Request input goes through Validate::*; responses through Http::json / Http::error ({"error":…,"code":…}).
  • No composer. If you think you need a dependency, vendor it or don't.
  • server/config.php is 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 — gh and PR workflows don't apply. Work lands on main.
  • Release = bump Version: in the plugin header and ATT_HC_VERSION and version + last_updated in updates.json, then push main. PUC polls updates.json and installs archive/main.zip, so whatever is on main is what ships. Built zips are gitignored.
  • steps.md is 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.