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

214 lines
9.8 KiB
Markdown

# Project Instructions for AI Agents
This file provides instructions and context for AI coding agents working on this project.
<!-- BEGIN BEADS INTEGRATION v:1 profile:minimal hash:ca08a54f -->
## Beads Issue Tracker
This project uses **bd (beads)** for issue tracking. Run `bd prime` to see full workflow context and commands.
### Quick Reference
```bash
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:
```bash
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
<!-- END BEADS INTEGRATION -->
## 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).
```bash
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.php` — **end-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):
```bash
# 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:
```bash
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
`return`ing 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_Session` — **the 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_Api` — `wp_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.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*` 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 SQLite**`config.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.