Central history server + plugin write-through sync (epic hc-0p1)

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>
This commit is contained in:
2026-06-29 12:43:24 +01:00
parent 6dd050ea1d
commit 1782e52504
26 changed files with 1548 additions and 37 deletions

View File

@@ -1,3 +1,10 @@
{"_type":"issue","id":"hc-cc8","title":"Plugin: per-step 'view previous notes' panel","description":"Each step card gets a small link/disclosure: 'Previous notes for this step (N)' where N is the count from the server.\n\nOn expand: render a list of past notes from GET /healthchecks/steps/{step_id}?site_key=\u003ccurrent\u003e\u0026limit=5. Each entry shows:\n- started_at (formatted)\n- status badge\n- notes (or 'no notes')\n- reporting_url if different from the current session\n\nLoaded lazily on first expand (one request per step) to avoid hammering the server on page load. Cache result for the page lifetime.","status":"closed","priority":1,"issue_type":"feature","assignee":"Steve Hanlon","owner":"steve@hanlon.co.uk","created_at":"2026-06-29T11:10:17Z","created_by":"Steve Hanlon","updated_at":"2026-06-29T11:41:06Z","started_at":"2026-06-29T11:36:10Z","closed_at":"2026-06-29T11:41:06Z","close_reason":"Per-step history disclosure shipped end-to-end.\n\nServer:\n- New GET /step-counts?site_key=...\u0026exclude_id=... returns {counts: {step_id: int}} for the (N) badge in one round-trip\n- stepHistory + stepCountsForSite both grew an exclude_id param so the active session doesn't appear in its own 'previous notes' panel\n- Routes: /step-counts moved to top-level path to avoid the /healthchecks/{id} pattern claiming 'step-counts' as an id\n\nPlugin:\n- ATT_HC_Api::step_counts() and step_history() pass exclude_id when available\n- att_hc_render_active_session pre-fetches step counts once and passes per-step count to the card renderer\n- Step card now renders a \u003cdetails class=att-hc-history data-step-id=…\u003e with summary 'Previous notes for this step (N)' when N\u003e0\n- New att_hc_print_step_history_assets prints the inline CSS + vanilla JS that hooks the details.toggle event: lazy-fetches on first expand via admin-ajax (action=att_hc_step_history), renders entries with date + status badge + notes + 'Reported from \u003curl\u003e' when different from current reporting_url. Marks data-loaded=yes to cache for page lifetime; resets to no on error so user can retry.\n- New wp_ajax_att_hc_step_history admin-ajax handler returns wp_send_json_success/error with nonce verification (att_hc_step_history nonce)\n\nVerified end-to-end against live server: counts correct with + without exclude_id (4 vs 3 for active vs prior), step_history filters active session, empty step / unknown site return cleanly, reordered routes still work.","dependencies":[{"issue_id":"hc-cc8","depends_on_id":"hc-0p1","type":"parent-child","created_at":"2026-06-29T12:13:24Z","created_by":"Steve Hanlon","metadata":"{}"},{"issue_id":"hc-cc8","depends_on_id":"hc-9jl","type":"blocks","created_at":"2026-06-29T12:10:37Z","created_by":"Steve Hanlon","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"hc-m1a","title":"Plugin: start screen — site_key field + recent dropdown + resume","description":"When no active local session exists, the start screen shows:\n\n1. Text input 'Site / engagement key' — defaults to normalised get_site_url() (lowercase, strip scheme + leading www., trim trailing slash). Editable. Help text: 'Used to group runs that span dev + live for the same engagement. Pick from recent engagements below if continuing one.'\n2. Dropdown 'Recent engagements' populated from GET /sites?limit=20. Selecting one fills the input.\n3. After site_key is set, GET /healthchecks?site_key=...\u0026include=steps. If any UNFINISHED sessions exist, list them with started_at and progress, each with a [Resume] button. Plus a separate [Start fresh] button.\n\nResume: load the chosen session into the local option (no new POST). Start fresh: new POST /healthchecks.","status":"closed","priority":1,"issue_type":"task","assignee":"Steve Hanlon","owner":"steve@hanlon.co.uk","created_at":"2026-06-29T11:10:05Z","created_by":"Steve Hanlon","updated_at":"2026-06-29T11:36:09Z","started_at":"2026-06-29T11:33:33Z","closed_at":"2026-06-29T11:36:09Z","close_reason":"Start screen rewritten:\n- Editable 'Site / engagement key' input, defaults to ATT_HC_Session::normalise_site_url(get_site_url()).\n- datalist-backed autocomplete from GET /sites?limit=20 (recent engagements). Picking one fills the input via native browser UX.\n- 'Look up engagement' submits as GET with site_key in the URL, so re-rendering picks it up and re-queries incompletes.\n- Table of in-progress sessions for the selected site_key with started_at, last-activity, reporting_url, and a [Resume] button per row. Each Resume button posts to a new att_hc_handle_resume handler which calls ATT_HC_Session::resume($id).\n- Separate Start button below labelled differently depending on whether incompletes exist.\n\nDouble-click guard: att_hc_handle_start (and att_hc_handle_resume) early-return to the main page if ATT_HC_Session::current() is non-null, so a stray repeat click can't 409 against the (site_key, started_at) unique constraint with a confusing duplicate error.\n\nEnd-to-end verified: /sites populates the datalist, incomplete filter shows 2 alpha + 1 beta + 0 unknown correctly, resume() repopulates cache and restores step notes, finish() removes from incomplete list. Lookup-err and recent-sites-err paths both surface as inline warnings without blocking the Start button.","dependencies":[{"issue_id":"hc-m1a","depends_on_id":"hc-0p1","type":"parent-child","created_at":"2026-06-29T12:13:23Z","created_by":"Steve Hanlon","metadata":"{}"},{"issue_id":"hc-m1a","depends_on_id":"hc-9jl","type":"blocks","created_at":"2026-06-29T12:10:36Z","created_by":"Steve Hanlon","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"hc-9jl","title":"Plugin: refactor ATT_HC_Session to write-through","description":"Session is now server-of-truth. Local WP option becomes a thin cache of the *active* session for fast page loads.\n\nChanges:\n- start(): generates id locally, POSTs to /healthchecks, stores response in option. site_key + reporting_url are new fields.\n- update_step(): updates option AND PUTs to /healthchecks/{id}/steps/{step_id}. If server call fails, the option is NOT updated and the caller sees the error (no drift).\n- finish(): PUT /healthchecks/{id} with finished_at, then update option.\n- set_autocheck(): same write-through pattern.\n- previous(): replace local 'att_hc_previous_session' option with a call to GET /healthchecks?site_key=...\u0026include=steps\u0026limit=2, return the most recent FINISHED one before the current session.\n- discard(): delete option only. Server keeps the record (intentional; history is the point).\n\nRemove the att_hc_previous_session option entirely (data is on the server now).","status":"closed","priority":1,"issue_type":"task","assignee":"Steve Hanlon","owner":"steve@hanlon.co.uk","created_at":"2026-06-29T11:09:55Z","created_by":"Steve Hanlon","updated_at":"2026-06-29T11:30:29Z","started_at":"2026-06-29T11:25:28Z","closed_at":"2026-06-29T11:30:29Z","close_reason":"ATT_HC_Session refactored to write-through. Every mutating method (start / update_step / finish / set_autocheck) POSTs or PUTs to the central server first; only on success does the local WP option cache get updated. ATT_HC_Api_Exception bubbles to callers — admin handlers (start / save_step / finish / refresh_checks / step_action) now wrap calls in try/catch and surface via att_hc_api_error transient → admin notice. Added: site_key field (defaults to normalise_site_url(get_site_url())), reporting_url field, ATT_HC_Session::resume(id) for hc-m1a, ATT_HC_Session::normalise_site_url(). previous() now fetches from server (list_healthchecks ?include=steps) and reconstructs into the local session shape; degrades silently on server unreachable since diff is a nice-to-have. Removed att_hc_previous_session option entirely. Also added a config-error gate at the top of the admin page that blocks the UI with a clear notice when wp-config.php constants are missing or wrong. Added server/dev-router.php to work around PHP -S 405-ing dotted uniqid paths (production Apache/nginx unaffected). End-to-end verified against live server: 13 assertions covering happy path, autocheck preservation across upserts, multi-session-per-site, previous()/resume() reconstruction, discard-preserves-server-record, and error bubbling.","dependencies":[{"issue_id":"hc-9jl","depends_on_id":"hc-0p1","type":"parent-child","created_at":"2026-06-29T12:13:23Z","created_by":"Steve Hanlon","metadata":"{}"},{"issue_id":"hc-9jl","depends_on_id":"hc-0rr","type":"blocks","created_at":"2026-06-29T12:10:36Z","created_by":"Steve Hanlon","metadata":"{}"}],"dependency_count":1,"dependent_count":2,"comment_count":0}
{"_type":"issue","id":"hc-0rr","title":"Plugin: HTTP client + config constants","description":"New class ATT_HC_Api in includes/class-att-hc-api.php.\n\nReads ATT_HC_API_URL and ATT_HC_API_KEY from wp-config.php constants. If either missing or ATT_HC_API_URL is not https://, plugin shows a blocking admin notice on Tools→Site Healthcheck and refuses to start sessions.\n\nMethods mirror server endpoints. wp_remote_post/get with timeout=15, Authorization header injected. On non-2xx, throws ATT_HC_Api_Exception with the server's error.code and human message. Caller (admin page) catches and surfaces.\n\nAcceptance:\n- Missing constants: clear notice, no PHP fatals\n- Wrong key (401): notice says 'server rejected our credentials'\n- Server down: notice says 'central history server unreachable — retry'","status":"closed","priority":1,"issue_type":"task","assignee":"Steve Hanlon","owner":"steve@hanlon.co.uk","created_at":"2026-06-29T11:09:45Z","created_by":"Steve Hanlon","updated_at":"2026-06-29T11:23:23Z","started_at":"2026-06-29T11:21:00Z","closed_at":"2026-06-29T11:23:23Z","close_reason":"ATT_HC_Api + ATT_HC_Api_Exception added and wired into the plugin bootstrap. Reads ATT_HC_API_URL / ATT_HC_API_KEY constants from wp-config.php. Methods: ping, create_healthcheck, update_healthcheck, get_healthcheck, list_healthchecks (with include=steps), upsert_step, step_history, recent_sites. Throws ATT_HC_Api_Exception with error_code + http_status on non-2xx or transport failure. Refuses non-HTTPS endpoints (loopback exception for dev). Verified end-to-end against live server: all 9 success paths round-trip, 409 + 404 error paths throw with correct code/message. No existing plugin behaviour changed yet — wiring in happens in hc-9jl.","dependencies":[{"issue_id":"hc-0rr","depends_on_id":"hc-0p1","type":"parent-child","created_at":"2026-06-29T12:13:22Z","created_by":"Steve Hanlon","metadata":"{}"},{"issue_id":"hc-0rr","depends_on_id":"hc-rdo","type":"blocks","created_at":"2026-06-29T12:10:36Z","created_by":"Steve Hanlon","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0}
{"_type":"issue","id":"hc-rdo","title":"Server: HTTP endpoints","description":"All routes require Authorization: Bearer \u003cATT_HC_API_KEY\u003e. JSON in/out. UTC unix timestamps.\n\nPOST /healthchecks Register new session. Body: {id, site_key, started_at, reporting_url, technician_id?, wp_version, php_version}. 201 on success, 409 if (site_key, started_at) collides.\nPUT /healthchecks/{id} Update session metadata. Body: {finished_at?}. 200 on success.\nPUT /healthchecks/{id}/steps/{step_id} Upsert step state. Body: {status, notes, autocheck?, reporting_url}. 200 on success.\nGET /healthchecks?site_key=...\u0026include=steps List sessions for a site, newest first. include=steps inlines step_updates.\nGET /healthchecks/{id} Full session including steps. For Resume flow.\nGET /healthchecks/steps/{step_id}?site_key=...\u0026limit=5 Notes for one step across last N sessions for this site. Newest first.\nGET /sites?limit=20 Recent distinct site_keys for the start-screen dropdown.\n\nErrors: {error: 'human message', code: 'machine_code'}. 401 for bad auth, 404 for unknown ids, 422 for validation, 500 for unexpected.","status":"closed","priority":1,"issue_type":"task","assignee":"Steve Hanlon","owner":"steve@hanlon.co.uk","created_at":"2026-06-29T11:09:35Z","created_by":"Steve Hanlon","updated_at":"2026-06-29T11:20:54Z","started_at":"2026-06-29T11:17:58Z","closed_at":"2026-06-29T11:20:54Z","close_reason":"All 7 endpoints implemented and smoke-tested end-to-end with curl: POST /healthchecks (201/409/422), GET /healthchecks/{id} (200/404), PUT /healthchecks/{id} (finish), PUT step (upsert with autocheck, 404/422), GET list (with optional include=steps, ordered DESC by started_at), GET step history across sessions, GET /sites recent keys. Multi-session per site_key works (different started_at). All 18 test cases passed including the dev-vs-live reporting_url tracking.","dependencies":[{"issue_id":"hc-rdo","depends_on_id":"hc-0p1","type":"parent-child","created_at":"2026-06-29T12:13:22Z","created_by":"Steve Hanlon","metadata":"{}"},{"issue_id":"hc-rdo","depends_on_id":"hc-7qm","type":"blocks","created_at":"2026-06-29T12:10:35Z","created_by":"Steve Hanlon","metadata":"{}"}],"dependency_count":1,"dependent_count":2,"comment_count":0}
{"_type":"issue","id":"hc-7qm","title":"Server: schema for healthchecks + step_updates + audit","description":"Tables:\n\nhealthchecks\n id TEXT PRIMARY KEY -- client-generated (uniqid from plugin)\n site_key TEXT NOT NULL -- composite key with started_at\n started_at INTEGER NOT NULL -- unix seconds\n finished_at INTEGER NULL\n technician_id INTEGER NULL -- WP user id, informational\n reporting_url TEXT NOT NULL -- get_site_url() at start\n wp_version TEXT\n php_version TEXT\n created_at INTEGER NOT NULL\n updated_at INTEGER NOT NULL\n UNIQUE (site_key, started_at)\n\nstep_updates\n healthcheck_id TEXT NOT NULL REFERENCES healthchecks(id) ON DELETE CASCADE\n step_id TEXT NOT NULL\n status TEXT NOT NULL -- not_started|done|skipped|blocked|n_a\n notes TEXT NOT NULL DEFAULT ''\n autocheck_json TEXT NULL -- JSON blob if step ran autocheck\n updated_at INTEGER NOT NULL\n reporting_url TEXT NOT NULL\n PRIMARY KEY (healthcheck_id, step_id)\n\nrequest_log (audit, simple)\n id INTEGER PRIMARY KEY AUTOINCREMENT\n ts INTEGER NOT NULL\n method TEXT\n path TEXT\n status INTEGER\n ip TEXT\n bytes_in INTEGER\n bytes_out INTEGER\n\nIndexes:\n- healthchecks(site_key, started_at DESC)\n- step_updates(step_id, updated_at DESC) -- for cross-session step history","status":"closed","priority":1,"issue_type":"task","assignee":"Steve Hanlon","owner":"steve@hanlon.co.uk","created_at":"2026-06-29T11:09:24Z","created_by":"Steve Hanlon","updated_at":"2026-06-29T11:17:57Z","started_at":"2026-06-29T11:17:02Z","closed_at":"2026-06-29T11:17:57Z","close_reason":"Schema + auto-migration runner in place. 0001_initial.sql creates healthchecks (unique (site_key, started_at)), step_updates (PK (healthcheck_id, step_id), FK cascades), request_log + indexes. Migrations table tracks applied files. Verified: cold start creates all 4 tables; second run is a no-op (1 row in migrations table). SQLite WAL + foreign_keys enabled in Db.php.","dependencies":[{"issue_id":"hc-7qm","depends_on_id":"hc-0p1","type":"parent-child","created_at":"2026-06-29T12:13:22Z","created_by":"Steve Hanlon","metadata":"{}"},{"issue_id":"hc-7qm","depends_on_id":"hc-r5r","type":"blocks","created_at":"2026-06-29T12:13:24Z","created_by":"Steve Hanlon","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0}
{"_type":"issue","id":"hc-r5r","title":"Server: scaffold PHP/SQLite app in server/","description":"Single-file router (index.php) + PDO + SQLite. No framework.\n\nLayout:\n- server/public/index.php — front controller, routing\n- server/src/ — Router, Auth, Store, controllers\n- server/migrations/ — SQL files run on boot if schema missing\n- server/config.php.example — copy to config.php on deploy, holds API key + DSN\n- server/.htaccess — route all to public/index.php\n\nAcceptance:\n- 'php -S localhost:8000 -t server/public' starts a working dev server\n- GET / returns {ok:true, version:...}\n- Missing/invalid Authorization header returns 401","status":"closed","priority":1,"issue_type":"task","assignee":"Steve Hanlon","owner":"steve@hanlon.co.uk","created_at":"2026-06-29T11:09:11Z","created_by":"Steve Hanlon","updated_at":"2026-06-29T11:13:25Z","started_at":"2026-06-29T11:10:44Z","closed_at":"2026-06-29T11:13:25Z","close_reason":"Scaffold complete: front controller + autoloader + Config + Http + Auth + Router + GET /. Dev server boots, GET / returns ok, Auth::require returns 401 for missing/invalid bearer and passes for valid.","dependencies":[{"issue_id":"hc-r5r","depends_on_id":"hc-0p1","type":"parent-child","created_at":"2026-06-29T12:13:21Z","created_by":"Steve Hanlon","metadata":"{}"}],"dependency_count":0,"dependent_count":1,"comment_count":0}
{"_type":"issue","id":"hc-5ix.26","title":"Decision: distribution model — internal-only, no WP.org listing","description":"This is an internal/agency tool, not a public plugin. Distributed as a built ZIP (GitHub release artefact). Technicians install via wp-admin → Plugins → Upload, or via wp-cli (`wp plugin install \u003curl\u003e --activate`).\n\n**Why:** lets us release fast, keeps client-specific text/checks private, no review process.\n\n**How to apply:**\n- Don't add WP.org boilerplate (readme.txt, screenshots, banner.png).\n- Plugin header version = source of truth for what's installed.\n- Phase 3 considers a self-hosted update channel (hc-5ix.NEW).","status":"open","priority":1,"issue_type":"decision","owner":"steve@hanlon.co.uk","created_at":"2026-06-11T14:40:29Z","created_by":"Steve Hanlon","updated_at":"2026-06-11T14:40:29Z","labels":["phase-1"],"dependencies":[{"issue_id":"hc-5ix.26","depends_on_id":"hc-5ix","type":"parent-child","created_at":"2026-06-11T15:40:28Z","created_by":"Steve Hanlon","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"hc-5ix.6","title":"Session lifecycle: Start/Resume/Finish actions","description":"Start: creates new session, snapshots site URL + WP/PHP versions + technician. Resume: continues the current in-progress session. Finish: stamps finished_at, locks notes, opens report view. If a session is already in-progress, Start asks to discard or resume.","notes":"Built in phase-1 scaffold; passing lint + WP-eval end-to-end smoke test on testsite.","status":"closed","priority":1,"issue_type":"task","owner":"steve@hanlon.co.uk","created_at":"2026-06-11T14:36:13Z","created_by":"Steve Hanlon","updated_at":"2026-06-11T14:49:28Z","started_at":"2026-06-11T14:41:28Z","closed_at":"2026-06-11T14:49:28Z","labels":["phase-1"],"dependencies":[{"issue_id":"hc-5ix.6","depends_on_id":"hc-5ix","type":"parent-child","created_at":"2026-06-11T15:36:12Z","created_by":"Steve Hanlon","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"hc-5ix.7","title":"Report generation: Markdown export + copy to clipboard + save to private CPT","description":"Render the finished session as Markdown (technician, site URL, started/finished, then per-step status + notes + sub-items). 'Copy' button and 'Save as report post' (private CPT 'wph_report') so reports are retrievable per site. Plain text fallback. No PDF in phase 1.","notes":"Built in phase-1 scaffold; passing lint + WP-eval end-to-end smoke test on testsite.","status":"closed","priority":1,"issue_type":"task","owner":"steve@hanlon.co.uk","created_at":"2026-06-11T14:36:13Z","created_by":"Steve Hanlon","updated_at":"2026-06-11T14:49:28Z","started_at":"2026-06-11T14:41:29Z","closed_at":"2026-06-11T14:49:28Z","labels":["phase-1"],"dependencies":[{"issue_id":"hc-5ix.7","depends_on_id":"hc-5ix","type":"parent-child","created_at":"2026-06-11T15:36:13Z","created_by":"Steve Hanlon","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0}
@@ -6,6 +13,8 @@
{"_type":"issue","id":"hc-5ix.4","title":"Session data model: option-backed in-progress healthcheck record","description":"One in-progress session per site at a time. Stored in a custom option (or a CPT — pick during implementation). Shape: id, started_at, finished_at, technician_id, site_url_snapshot, per_step_state { status: not_started|in_progress|done|skipped|blocked|n_a, notes, completed_at }. Designed so phase-3 automation can attach structured findings later.","notes":"Built in phase-1 scaffold; passing lint + WP-eval end-to-end smoke test on testsite.","status":"closed","priority":1,"issue_type":"task","owner":"steve@hanlon.co.uk","created_at":"2026-06-11T14:36:11Z","created_by":"Steve Hanlon","updated_at":"2026-06-11T14:49:27Z","started_at":"2026-06-11T14:41:27Z","closed_at":"2026-06-11T14:49:27Z","labels":["phase-1"],"dependencies":[{"issue_id":"hc-5ix.4","depends_on_id":"hc-5ix","type":"parent-child","created_at":"2026-06-11T15:36:11Z","created_by":"Steve Hanlon","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"hc-5ix.2","title":"Plugin skeleton: header, activation hook, deactivation hook, admin menu (Tools → Site Healthcheck), capability gate","notes":"Built in phase-1 scaffold; passing lint + WP-eval end-to-end smoke test on testsite.","status":"closed","priority":1,"issue_type":"task","owner":"steve@hanlon.co.uk","created_at":"2026-06-11T14:36:10Z","created_by":"Steve Hanlon","updated_at":"2026-06-11T14:49:26Z","started_at":"2026-06-11T14:41:27Z","closed_at":"2026-06-11T14:49:26Z","labels":["phase-1"],"dependencies":[{"issue_id":"hc-5ix.2","depends_on_id":"hc-5ix","type":"parent-child","created_at":"2026-06-11T15:36:09Z","created_by":"Steve Hanlon","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"hc-5ix.1","title":"Bootstrap: trigger install of wp-site-recovery plugin as step 0","description":"Healthcheck plugin should check on activation if site-recovery is installed; if not, fetch and install from a known URL/ZIP and activate it. Block stepper from starting until recovery is in place.","notes":"Detection half done (status panel + manual link). Auto-install of recovery plugin from a private URL is the wp-site-recovery side of hc-5ix.27 — closing this as 'detection complete'.","status":"closed","priority":1,"issue_type":"task","owner":"steve@hanlon.co.uk","created_at":"2026-06-11T11:26:40Z","created_by":"Steve Hanlon","updated_at":"2026-06-11T15:03:02Z","closed_at":"2026-06-11T15:03:02Z","labels":["phase-1"],"dependencies":[{"issue_id":"hc-5ix.1","depends_on_id":"hc-5ix","type":"parent-child","created_at":"2026-06-11T12:26:39Z","created_by":"Steve Hanlon","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"hc-3y0","title":"Server: deploy notes for VPS","description":"Document deployment to a PHP VPS:\n- PHP version requirements\n- Apache/nginx rewrite to public/index.php\n- File perms for SQLite db file\n- Suggested location (/var/www/att-hc-server/)\n- Generating + setting ATT_HC_API_KEY\n- Backup strategy for the SQLite file (cron + scp/rsync)\n- HTTPS via Let's Encrypt\n\nLives at server/DEPLOY.md.","status":"closed","priority":2,"issue_type":"task","assignee":"Steve Hanlon","owner":"steve@hanlon.co.uk","created_at":"2026-06-29T11:10:27Z","created_by":"Steve Hanlon","updated_at":"2026-06-29T11:23:22Z","started_at":"2026-06-29T11:21:00Z","closed_at":"2026-06-29T11:23:22Z","close_reason":"DEPLOY.md written: requirements, layout, first-time install (rsync + key generation + perms), Apache + nginx vhost templates with Authorization header forwarding, Let's Encrypt, plugin-side wp-config.php constants, SQLite online backup cron, update procedure, health-check curl.","dependencies":[{"issue_id":"hc-3y0","depends_on_id":"hc-0p1","type":"parent-child","created_at":"2026-06-29T12:13:24Z","created_by":"Steve Hanlon","metadata":"{}"},{"issue_id":"hc-3y0","depends_on_id":"hc-rdo","type":"blocks","created_at":"2026-06-29T12:10:37Z","created_by":"Steve Hanlon","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"hc-0p1","title":"[epic] Central history server + plugin sync","description":"Replace the local-only session model with a required central server that stores healthcheck history across engagements.\n\nMotivation:\n- Plugin is installed per engagement and uninstalled at the end, so today's local 'previous session' diff dies with it\n- Healthchecks differ between sites; cross-engagement history lets us see prior notes per step and review what's drifted\n- Engagements often span dev + live (different URLs, same logical site), and may run over days/weeks — sometimes never completed\n\nScope:\n- New PHP/SQLite server in server/ (same repo), deployed to a VPS\n- Plugin becomes a write-through client: server is source of truth\n- DB key = (site_key, started_at); site_key defaults to normalised get_site_url() but is editable on start\n- Multiple incomplete sessions per site allowed; on start, tech can Resume or Start fresh\n- Each step card gets a 'view previous notes' link that pulls history from the server\n- Auth: single shared secret as ATT_HC_API_KEY constant in wp-config.php\n- HTTPS required; plain HTTP rejected client-side\n- Server unreachable = Start/Save/Finish block with retry. No offline queue v1.","status":"open","priority":2,"issue_type":"feature","owner":"steve@hanlon.co.uk","created_at":"2026-06-29T11:09:02Z","created_by":"Steve Hanlon","updated_at":"2026-06-29T11:09:02Z","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"hc-5ix.28","title":"Step — Email Delivery Test (send wp_mail to chosen address)","description":"Add a new step that sends a test email via wp_mail() to an address the technician types in. Should detect SMTP plugins in use, surface wp_mail_failed errors, and store the result as a finding so it lands in the report.\n\nDrives a small architectural extension: WPH_Step gains optional render_extra() (output HTML inside the card) and handle_action() (handle a step-specific POST), plus a generic wph_step_action admin-post handler. Demonstrates the drop-in extensibility — adding the step is a single file plus a tiny hook addition.","notes":"Added in commit (next). New file includes/steps/115-email.php — drop-in step, no other source changes beyond the small WPH_Step extension (render_extra + handle_action) and one admin-post handler (wph_step_action). Demonstrates the extensibility pattern: a step can declare its own form + handler without touching any other file.\n\nSmoke-tested on testsite: step appears in correct slot (between small_fixes and wrap_up), autocheck surfaces mailer detection (PHP mail vs. 7 known SMTP plugins) and default From address, render_extra outputs a To: input prefilled with current user's email, handle_action rejects invalid addresses and successfully sends via wp_mail (caught by Local's MailHog).","status":"closed","priority":2,"issue_type":"task","owner":"steve@hanlon.co.uk","created_at":"2026-06-11T17:43:34Z","created_by":"Steve Hanlon","updated_at":"2026-06-11T17:45:47Z","closed_at":"2026-06-11T17:45:47Z","labels":["phase-1"],"dependencies":[{"issue_id":"hc-5ix.28","depends_on_id":"hc-5ix","type":"parent-child","created_at":"2026-06-11T18:43:33Z","created_by":"Steve Hanlon","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"hc-5ix.9","title":"Smoke test on testsite: full end-to-end run through the checklist","notes":"Built in phase-1 scaffold; passing lint + WP-eval end-to-end smoke test on testsite.","status":"closed","priority":2,"issue_type":"task","owner":"steve@hanlon.co.uk","created_at":"2026-06-11T14:36:15Z","created_by":"Steve Hanlon","updated_at":"2026-06-11T14:49:29Z","closed_at":"2026-06-11T14:49:29Z","labels":["phase-1"],"dependencies":[{"issue_id":"hc-5ix.9","depends_on_id":"hc-5ix","type":"parent-child","created_at":"2026-06-11T15:36:14Z","created_by":"Steve Hanlon","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"hc-5ix.8","title":"README + install instructions + screenshots placeholder","notes":"Built in phase-1 scaffold; passing lint + WP-eval end-to-end smoke test on testsite.","status":"closed","priority":2,"issue_type":"chore","owner":"steve@hanlon.co.uk","created_at":"2026-06-11T14:36:14Z","created_by":"Steve Hanlon","updated_at":"2026-06-11T14:49:29Z","closed_at":"2026-06-11T14:49:29Z","labels":["phase-1"],"dependencies":[{"issue_id":"hc-5ix.8","depends_on_id":"hc-5ix","type":"parent-child","created_at":"2026-06-11T15:36:14Z","created_by":"Steve Hanlon","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0}