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.
214 lines
9.8 KiB
Markdown
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.
|