diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 2dcc495..655411e 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -1,4 +1,4 @@ -{"_type":"issue","id":"hc-5yy","title":"AJAX step-save listeners never attach (script runs before cards render)","description":"0.1.5 shipped the AJAX auto-save, but the inline script is emitted before the step-card markup, so document.querySelectorAll('form.att-hc-step-save') matches nothing and no change/blur listeners attach — no network request fires. The pre-existing step-history loader shared the same defect. Fixed by deferring both scripts' wiring to DOMContentLoaded. Released in 0.1.6.","status":"open","priority":1,"issue_type":"bug","owner":"steve@hanlon.co.uk","created_at":"2026-07-27T07:36:54Z","created_by":"Steve Hanlon","updated_at":"2026-07-27T07:36:54Z","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"hc-5yy","title":"AJAX step-save listeners never attach (script runs before cards render)","description":"0.1.5 shipped the AJAX auto-save, but the inline script is emitted before the step-card markup, so document.querySelectorAll('form.att-hc-step-save') matches nothing and no change/blur listeners attach — no network request fires. The pre-existing step-history loader shared the same defect. Fixed by deferring both scripts' wiring to DOMContentLoaded. Released in 0.1.6.","status":"closed","priority":1,"issue_type":"bug","owner":"steve@hanlon.co.uk","created_at":"2026-07-27T07:36:54Z","created_by":"Steve Hanlon","updated_at":"2026-07-27T07:37:07Z","closed_at":"2026-07-27T07:37:07Z","close_reason":"Fixed in 0.1.6 — both inline scripts now defer listener wiring to DOMContentLoaded.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"hc-4m5","title":"Plugin: allow ATT_HC_API_URL / ATT_HC_API_KEY via settings screen (not just wp-config)","description":"Some managed hosts make editing wp-config.php impractical or explicitly forbid it (WPE, some resellers, some clients' own operations teams). Add a settings-screen alternative to the existing constants.\n\nDesign (mirror the pattern already used for the Gitea recovery config on the same settings page):\n\n- Two new WP options: att_hc_api_url and att_hc_api_key (autoload=false on the key).\n- ATT_HC_Api::url() and ATT_HC_Api::key() accessors: return the constant if defined + non-empty, else the option. All existing consumers switch to these accessors instead of the constants directly.\n- config_error() message updates to say 'add to wp-config.php OR set via Tools -\u003e Site Healthcheck -\u003e Settings'.\n- Settings page gets a new 'Central history server' card above the existing Gitea card, with:\n - URL input (type=url) — locked with 'Set via ATT_HC_API_URL constant' description when constant is defined\n - Key input (type=password, autocomplete=new-password) — same lock behaviour\n- Save handler extends the existing att_hc_save_settings to persist the two options.\n- No migration needed — sites using constants keep working untouched.\n\nSecurity note to include in the description on the settings page: the option-stored key is visible to any WP admin (options.php, DB) — the constant path is stronger. Recommend constant when possible.\n\nUser asked for this because some sites can't edit wp-config.php.","status":"closed","priority":1,"issue_type":"feature","assignee":"Steve Hanlon","owner":"steve@hanlon.co.uk","created_at":"2026-07-07T13:51:13Z","created_by":"Steve Hanlon","updated_at":"2026-07-07T13:54:15Z","started_at":"2026-07-07T13:51:24Z","closed_at":"2026-07-07T13:54:15Z","close_reason":"Added a 'Central history server' card at the top of the settings page (Tools → Site Healthcheck → Settings) with URL + API key fields. Storage: att_hc_api_url and att_hc_api_key options (autoload=false on the key). Resolver: ATT_HC_Api::url() / ATT_HC_Api::key() return the constant when defined and non-empty, else the option, else ''. url_from_constant() / key_from_constant() drive the per-field lock on the settings page. All existing consumers (request(), config_error(), is_configured(), the config-error notice) switched to the accessors. Separate action + nonce (att_hc_save_api_settings) so it doesn't tangle with the existing gitea save. Handler ignores submitted values for fields locked by a constant — belt-and-braces with the disabled input. 18 assertions verified: no-config / options-only / http-blocked / loopback-http-ok / constant-wins-over-option. Both PHP 8.3 and PHP 7.4 parse cleanly.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"hc-eff","title":"Plugin fatals on PHP 7.4 — PHP 8 syntax in central-server work","description":"The central history server work (epic hc-0p1) introduced four PHP 8.0+ syntax sites in the plugin codebase. PHP 7.4 fails to parse the affected files, so on a host running PHP 7.4 (eg one user reported Ubuntu 7.4.3-4ubuntu2.29) the plugin throws a fatal on load.\n\nSites:\n- includes/class-att-hc-session.php:244 — str_starts_with()\n- includes/class-att-hc-api.php:52 — named argument requires_auth: false\n- includes/class-att-hc-session.php:126 — named arguments include_steps:, limit:\n- includes/admin-page.php:165 — named arguments include_steps:, limit:\n\nPlugin main file header still says 'Requires PHP: 7.4', so the expectation is PHP 7.4 support. Backport: replace str_starts_with with substr === comparison; convert named args to positional (signatures accept positional already).\n\nAffects every plugin user on PHP \u003c 8.0.","status":"closed","priority":1,"issue_type":"bug","assignee":"Steve Hanlon","owner":"steve@hanlon.co.uk","created_at":"2026-06-30T08:27:51Z","created_by":"Steve Hanlon","updated_at":"2026-06-30T08:29:22Z","started_at":"2026-06-30T08:27:57Z","closed_at":"2026-06-30T08:29:22Z","close_reason":"Backported four PHP 8.0+ syntax sites to PHP 7.4-compatible equivalents. str_starts_with replaced with substr(...) === literal. Three named-argument call sites converted to positional — signatures already accept positional with the same values, no semantic change. Verified by linting all 16 plugin files (att-site-healthcheck.php + includes/) against PHP 7.4.33 on sjh: no syntax errors, no residual PHP 8+ patterns. Plugin's 'Requires PHP: 7.4' header is now actually true.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_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} @@ -29,6 +29,7 @@ {"_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} {"_type":"issue","id":"hc-5ix","title":"Build WordPress Healthcheck plugin (stepper through steps.md)","description":"Plugin that walks a technician through the WordPress healthcheck steps documented in steps.md. Independent of the recovery plugin (which it depends on as step 0).","status":"open","priority":2,"issue_type":"epic","owner":"steve@hanlon.co.uk","created_at":"2026-06-11T11:26:32Z","created_by":"Steve Hanlon","updated_at":"2026-06-11T11:26:32Z","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"hc-l0j","title":"Sidebar step dots track status changes live","description":"When a step status is saved via AJAX, recolour the sidebar step-list dot (and, via the existing :has() rule, the link text green/blue) live instead of only on page refresh. Extends applyStatus() in the step-save script; sidebar dots gained a data-step attribute. Released in 0.1.7.","status":"open","priority":3,"issue_type":"feature","owner":"steve@hanlon.co.uk","created_at":"2026-07-27T07:42:04Z","created_by":"Steve Hanlon","updated_at":"2026-07-27T07:42:04Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"hc-ufl","title":"Server: add rate limiting + lockout on API + /dashboard, rotate error_log","description":"Central healthcheck history server has no rate limiting or account lockout:\n\n- `server/src/Auth.php` — unlimited Bearer-key attempts, no delay, no lockout\n- `server/src/Dashboard.php:46-71` — unlimited login attempts on the /dashboard cookie-key form\n\nCombined with the shared-API-key exposure (see companion issue), a leaked or brute-forced key can be abused indefinitely with no signal.\n\nAlso: `server/src/bootstrap.php:25` calls `error_log()` on every uncaught exception with no rotation — the log grows unbounded on a busy or attacked server.","design":"**Rate limiting**: simplest workable option is a small in-DB counter table (key = client IP + endpoint, value = attempt count + window start). Reject when \u003e N attempts / window. No Redis dependency needed at current scale.\n\n**Lockout on /dashboard**: after e.g. 5 failed attempts from one IP within 15 min, return 429 with Retry-After. Keep it IP-scoped (not global) so a random attacker can't lock out real users.\n\n**Auth on API endpoints**: constant-time compare is already correct (`Auth.php:20` uses `hash_equals`). Add: increment failed-attempt counter, refuse to serve after threshold. Consider structured logging of each rejection with IP + user-agent for later review.\n\n**Log rotation**: either delegate to logrotate on the host (document in DEPLOY.md) OR self-rotate via a size check + rename in `bootstrap.php`'s handler.\n\nNice-to-have: session hardening on /dashboard — call `session_regenerate_id(true)` after successful auth (currently missing at `Dashboard.php:53-70`).","acceptance_criteria":"- [ ] API + /dashboard rate-limited per IP with a documented threshold\n- [ ] 429 with Retry-After returned when limit hit\n- [ ] error_log rotation strategy in place (either OS-level via DEPLOY.md docs, or in-app)\n- [ ] session_regenerate_id after /dashboard auth\n- [ ] Rejections logged with enough context to spot abuse patterns","notes":"Reported in 2026-07-16 security review. Flagged as risk #3 of 3 — lower priority than the API-key and recovery-installer issues but worth tackling in the same sweep since it's server-side and cheap.\n\nAlso noted (not required for close): session fixation on /dashboard is theoretically possible but requires attacker to first set a victim's PHPSESSID — high bar. Adding session_regenerate_id closes it cheaply.","status":"open","priority":3,"issue_type":"bug","owner":"steve@hanlon.co.uk","created_at":"2026-07-16T10:14:51Z","created_by":"Steve Hanlon","updated_at":"2026-07-16T10:14:51Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"hc-u5c","title":"Add Google Analytics and Search Console healthcheck steps","description":"Add two new healthcheck steps between performance (Step 7) and security (Step 8): a Google Analytics check that sniffs the homepage for GA4/GTM/UA snippets and detects known analytics plugins, and a Google Search Console check that looks for verification meta tags, sitemap reachability, robots.txt Disallow rules, and the WP 'Discourage search engines' setting. Updates steps.md to match.","status":"closed","priority":3,"issue_type":"feature","assignee":"Steve Hanlon","owner":"steve@hanlon.co.uk","created_at":"2026-07-16T08:44:39Z","created_by":"Steve Hanlon","updated_at":"2026-07-16T08:44:56Z","started_at":"2026-07-16T08:44:44Z","closed_at":"2026-07-16T08:44:56Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"hc-5ix.27","title":"Self-hosted update channel: plugin checks a private URL for new releases and offers one-click update from wp-admin","description":"Lightweight wrapper around the WP plugin update transient: site_transient_update_plugins filter that adds an entry for ourselves if a newer release exists at a configured URL. URL hosts a JSON manifest + zip. Lets technicians keep clients up to date without manually re-uploading the zip every check.","status":"open","priority":3,"issue_type":"task","owner":"steve@hanlon.co.uk","created_at":"2026-06-11T14:40:30Z","created_by":"Steve Hanlon","updated_at":"2026-06-11T14:40:30Z","labels":["phase-3"],"dependencies":[{"issue_id":"hc-5ix.27","depends_on_id":"hc-5ix","type":"parent-child","created_at":"2026-06-11T15:40:29Z","created_by":"Steve Hanlon","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} diff --git a/att-site-healthcheck.php b/att-site-healthcheck.php index 62d9216..9dba3ec 100644 --- a/att-site-healthcheck.php +++ b/att-site-healthcheck.php @@ -2,7 +2,7 @@ /** * Plugin Name: ATT Site Healthcheck * Description: Walks a technician through a structured WordPress site healthcheck. Steps are drop-in PHP files so adding/removing one is a single file change. - * Version: 0.1.6 + * Version: 0.1.7 * Author: Steve Hanlon * License: Proprietary * License URI: https://git.h12e.com/steve/wp-healthcheck/raw/branch/main/LICENSE @@ -15,7 +15,7 @@ if (!defined('ABSPATH')) { exit; } -define('ATT_HC_VERSION', '0.1.6'); +define('ATT_HC_VERSION', '0.1.7'); define('ATT_HC_PLUGIN_FILE', __FILE__); define('ATT_HC_PLUGIN_DIR', plugin_dir_path(__FILE__)); define('ATT_HC_PLUGIN_URL', plugin_dir_url(__FILE__)); diff --git a/includes/admin-page.php b/includes/admin-page.php index 3ca0463..958382a 100644 --- a/includes/admin-page.php +++ b/includes/admin-page.php @@ -508,8 +508,10 @@ function att_hc_print_step_save_assets(ATT_HC_Session $session): void { }); } - // Reflect a status change in the header badge + escalation banner, - // matching what a full server render would have produced. + // Reflect a status change in the header badge, escalation banner and + // sidebar dot, matching what a full server render would have produced. + // Recolouring the sidebar dot's class is enough for the link text too: + // the `li:has(.dot-done) a` CSS rule turns it green / back to default. function applyStatus(status, label) { if (card) { var badge = card.querySelector('header .att-hc-step-status'); @@ -520,6 +522,8 @@ function att_hc_print_step_save_assets(ATT_HC_Session $session): void { var esc = card.querySelector('[data-role="escalation"]'); if (esc) esc.hidden = (status !== 'blocked'); } + var dot = document.querySelector('.att-hc-sidebar .dot[data-step="' + stepId + '"]'); + if (dot) dot.className = 'dot dot-' + status; } select.addEventListener('change', function () { save(select); }); @@ -546,9 +550,10 @@ function att_hc_render_sidebar(ATT_HC_Session $session): void { $state = $session->step_state($step->id()); $cls = 'dot-' . $state['status']; printf( - '