diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl new file mode 100644 index 0000000..73a85e6 --- /dev/null +++ b/.beads/issues.jsonl @@ -0,0 +1,28 @@ +{"_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} +{"_type":"issue","id":"hc-5ix.5","title":"Checklist UI: single admin page with section per step, checkbox group + notes textarea","description":"One page, all 12 steps + before-you-start. Each step is a card with: title, sub-items as readable bullets, a status select (not started/done/skipped/blocked/n/a), a notes textarea. Saves on blur or via explicit Save. Shows the 'Watch out for' callouts from steps.md inline.","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:12Z","created_by":"Steve Hanlon","updated_at":"2026-06-11T14:49:27Z","started_at":"2026-06-11T14:41:28Z","closed_at":"2026-06-11T14:49:27Z","labels":["phase-1"],"dependencies":[{"issue_id":"hc-5ix.5","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.3","title":"Step definitions module: single PHP file defining the 12 steps + 'Before You Start' as structured data (id, title, blurb, sub-items, escalation conditions)","description":"Source of truth for both the MVP UI and the automation layer. Keep it boring: pure data, no presentation. Lives at includes/steps.php returning a typed array. The text comes from steps.md (current copy in repo root).","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:26Z","started_at":"2026-06-11T14:41:27Z","closed_at":"2026-06-11T14:49:26Z","labels":["phase-1"],"dependencies":[{"issue_id":"hc-5ix.3","depends_on_id":"hc-5ix","type":"parent-child","created_at":"2026-06-11T15:36:10Z","created_by":"Steve Hanlon","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"_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 in place via WPH_Recovery_Bootstrap (plugin slug check + is_plugin_active). Phase-1 surfaces installed/active/missing state with link to recovery settings. One-click install from a private URL deferred to hc-5ix.27.","status":"open","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-11T14:49:29Z","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-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-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} +{"_type":"issue","id":"hc-5ix.25","title":"Step 12 (Wrap-up): pre-fill report with all automated findings, attach diff vs. previous session, optionally push summary to ManageWP/WP Umbrella","status":"open","priority":3,"issue_type":"task","owner":"steve@hanlon.co.uk","created_at":"2026-06-11T14:36:26Z","created_by":"Steve Hanlon","updated_at":"2026-06-11T14:36:26Z","labels":["phase-3"],"dependencies":[{"issue_id":"hc-5ix.25","depends_on_id":"hc-5ix","type":"parent-child","created_at":"2026-06-11T15:36:25Z","created_by":"Steve Hanlon","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"hc-5ix.24","title":"Step 11 (Small fixes): broken internal link scan (sample first N pages), missing alt-text scan on homepage images, deactivated-but-installed plugin list","status":"open","priority":3,"issue_type":"task","owner":"steve@hanlon.co.uk","created_at":"2026-06-11T14:36:25Z","created_by":"Steve Hanlon","updated_at":"2026-06-11T14:36:25Z","labels":["phase-3"],"dependencies":[{"issue_id":"hc-5ix.24","depends_on_id":"hc-5ix","type":"parent-child","created_at":"2026-06-11T15:36:25Z","created_by":"Steve Hanlon","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"hc-5ix.22","title":"Step 9 (Database): table sizes report, spam comment count, post revision count, autoload option size, recommendation engine (e.g. 'consider revision limit if \u003e5000')","status":"open","priority":3,"issue_type":"task","owner":"steve@hanlon.co.uk","created_at":"2026-06-11T14:36:24Z","created_by":"Steve Hanlon","updated_at":"2026-06-11T14:36:24Z","labels":["phase-3"],"dependencies":[{"issue_id":"hc-5ix.22","depends_on_id":"hc-5ix","type":"parent-child","created_at":"2026-06-11T15:36:23Z","created_by":"Steve Hanlon","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"hc-5ix.23","title":"Step 10 (Uptime): pluggable uptime provider integration (ManageWP, UptimeRobot, BetterStack, Pingdom). At minimum expose 'paste downtime summary' field","status":"open","priority":3,"issue_type":"task","owner":"steve@hanlon.co.uk","created_at":"2026-06-11T14:36:24Z","created_by":"Steve Hanlon","updated_at":"2026-06-11T14:36:24Z","labels":["phase-3"],"dependencies":[{"issue_id":"hc-5ix.23","depends_on_id":"hc-5ix","type":"parent-child","created_at":"2026-06-11T15:36:24Z","created_by":"Steve Hanlon","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"hc-5ix.21","title":"Step 8 (Security): SSL cert expiry check (stream_socket_client to :443 + parse cert), admin user audit (flag unfamiliar accounts vs. snapshot baseline), xmlrpc.php reachability check, custom login URL check","status":"open","priority":3,"issue_type":"task","owner":"steve@hanlon.co.uk","created_at":"2026-06-11T14:36:23Z","created_by":"Steve Hanlon","updated_at":"2026-06-11T14:36:23Z","labels":["phase-3"],"dependencies":[{"issue_id":"hc-5ix.21","depends_on_id":"hc-5ix","type":"parent-child","created_at":"2026-06-11T15:36:23Z","created_by":"Steve Hanlon","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"hc-5ix.19","title":"Step 6 (Visual \u0026 Functional): homepage screenshot via mShots (or local headless if available); checklist with auto-pulled key pages (front page + posts page + WC shop + login)","status":"open","priority":3,"issue_type":"task","owner":"steve@hanlon.co.uk","created_at":"2026-06-11T14:36:22Z","created_by":"Steve Hanlon","updated_at":"2026-06-11T14:36:22Z","labels":["phase-3"],"dependencies":[{"issue_id":"hc-5ix.19","depends_on_id":"hc-5ix","type":"parent-child","created_at":"2026-06-11T15:36:21Z","created_by":"Steve Hanlon","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"hc-5ix.20","title":"Step 7 (Performance): PageSpeed Insights API integration (server-side fetch), record mobile + desktop scores, flag \u003e10pt drop vs. previous session, image scan for uncompressed \u003e 500KB on homepage","description":"Requires a Google PageSpeed API key (per-installation setting). Fall back to manually-entered scores if no key. Cache results for 12h.","status":"open","priority":3,"issue_type":"task","owner":"steve@hanlon.co.uk","created_at":"2026-06-11T14:36:22Z","created_by":"Steve Hanlon","updated_at":"2026-06-11T14:36:22Z","labels":["phase-3"],"dependencies":[{"issue_id":"hc-5ix.20","depends_on_id":"hc-5ix","type":"parent-child","created_at":"2026-06-11T15:36:22Z","created_by":"Steve Hanlon","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"hc-5ix.18","title":"Step 5 (Theme): detect parent/child relationship; if no child, diff parent theme files vs. WP.org canonical to flag direct customisations that would be lost on update","status":"open","priority":3,"issue_type":"task","owner":"steve@hanlon.co.uk","created_at":"2026-06-11T14:36:21Z","created_by":"Steve Hanlon","updated_at":"2026-06-11T14:36:21Z","labels":["phase-3"],"dependencies":[{"issue_id":"hc-5ix.18","depends_on_id":"hc-5ix","type":"parent-child","created_at":"2026-06-11T15:36:20Z","created_by":"Steve Hanlon","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"hc-5ix.16","title":"Step 3 (Core): wizard around core update with safe-mode (deactivate plugins first, update, smoke-load /, reactivate) and clear rollback path on failure","status":"open","priority":3,"issue_type":"task","owner":"steve@hanlon.co.uk","created_at":"2026-06-11T14:36:20Z","created_by":"Steve Hanlon","updated_at":"2026-06-11T14:36:20Z","labels":["phase-3"],"dependencies":[{"issue_id":"hc-5ix.16","depends_on_id":"hc-5ix","type":"parent-child","created_at":"2026-06-11T15:36:19Z","created_by":"Steve Hanlon","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"hc-5ix.17","title":"Step 4 (Plugins): enrich each plugin with WP.org 'last updated' date, 'removed from repo' flag, active install count; flag plugins not updated \u003e12 months; flag plugins missing from repo","description":"Calls api.wordpress.org/plugins/info/1.0/\u003cslug\u003e.json for each plugin. Cache per-site for 24h. 'Removed from repo' = 404 or unsupported response.","status":"open","priority":3,"issue_type":"task","owner":"steve@hanlon.co.uk","created_at":"2026-06-11T14:36:20Z","created_by":"Steve Hanlon","updated_at":"2026-06-11T14:36:20Z","labels":["phase-3"],"dependencies":[{"issue_id":"hc-5ix.17","depends_on_id":"hc-5ix","type":"parent-child","created_at":"2026-06-11T15:36:20Z","created_by":"Steve Hanlon","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"hc-5ix.15","title":"Step 2 (Environment): auto-collect PHP version + EOL flag, disk usage (statvfs), tail of error log (PHP + WP debug.log if enabled), wp-config flags (WP_DEBUG, WP_DEBUG_DISPLAY, DISALLOW_FILE_EDIT), spot-check permissions on wp-config.php / wp-content / uploads","status":"open","priority":3,"issue_type":"task","owner":"steve@hanlon.co.uk","created_at":"2026-06-11T14:36:19Z","created_by":"Steve Hanlon","updated_at":"2026-06-11T14:36:19Z","labels":["phase-3"],"dependencies":[{"issue_id":"hc-5ix.15","depends_on_id":"hc-5ix","type":"parent-child","created_at":"2026-06-11T15:36:18Z","created_by":"Steve Hanlon","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"hc-5ix.14","title":"Step 1 (Backup): detect installed backup plugin (UpdraftPlus, BackWPup, BlogVault, Jetpack VaultPress), surface last backup time + size + verification status","status":"open","priority":3,"issue_type":"task","owner":"steve@hanlon.co.uk","created_at":"2026-06-11T14:36:18Z","created_by":"Steve Hanlon","updated_at":"2026-06-11T14:36:18Z","labels":["phase-3"],"dependencies":[{"issue_id":"hc-5ix.14","depends_on_id":"hc-5ix","type":"parent-child","created_at":"2026-06-11T15:36:18Z","created_by":"Steve Hanlon","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"hc-5ix.12","title":"Diff against previous session: highlight deltas vs. last completed session for this site","description":"Pre-fill 'before' values for plugin/theme/WP versions from the previous session's 'after' values. Show a Δ column on the report. Lets clients see trajectory across checks.","status":"open","priority":3,"issue_type":"task","owner":"steve@hanlon.co.uk","created_at":"2026-06-11T14:36:17Z","created_by":"Steve Hanlon","updated_at":"2026-06-11T14:36:17Z","labels":["phase-3"],"dependencies":[{"issue_id":"hc-5ix.12","depends_on_id":"hc-5ix","type":"parent-child","created_at":"2026-06-11T15:36:16Z","created_by":"Steve Hanlon","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"hc-5ix.13","title":"Polished HTML/PDF report styling + ability to email","status":"open","priority":3,"issue_type":"task","owner":"steve@hanlon.co.uk","created_at":"2026-06-11T14:36:17Z","created_by":"Steve Hanlon","updated_at":"2026-06-11T14:36:17Z","labels":["phase-3"],"dependencies":[{"issue_id":"hc-5ix.13","depends_on_id":"hc-5ix","type":"parent-child","created_at":"2026-06-11T15:36:17Z","created_by":"Steve Hanlon","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"hc-5ix.11","title":"Stop-and-escalate decision support: when a step is marked blocked/escalate, surface the matching guidance from steps.md and link to a quote/escalation template","description":"Drive from the 'When to Stop and Escalate' table at the bottom of steps.md. Each escalation condition becomes a structured rule that fires when its preconditions hold (e.g. step=backup, status=blocked → 'Stop. Do not proceed. Restore backup before retry.'). Render as a banner on the affected step.","status":"open","priority":3,"issue_type":"task","owner":"steve@hanlon.co.uk","created_at":"2026-06-11T14:36:16Z","created_by":"Steve Hanlon","updated_at":"2026-06-11T14:36:16Z","labels":["phase-3"],"dependencies":[{"issue_id":"hc-5ix.11","depends_on_id":"hc-5ix","type":"parent-child","created_at":"2026-06-11T15:36:16Z","created_by":"Steve Hanlon","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"hc-5ix.10","title":"Linear stepper UI: replace single-page with one-step-per-page + progress bar + per-step timer","status":"open","priority":3,"issue_type":"task","owner":"steve@hanlon.co.uk","created_at":"2026-06-11T14:36:15Z","created_by":"Steve Hanlon","updated_at":"2026-06-11T14:36:15Z","labels":["phase-3"],"dependencies":[{"issue_id":"hc-5ix.10","depends_on_id":"hc-5ix","type":"parent-child","created_at":"2026-06-11T15:36:15Z","created_by":"Steve Hanlon","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} diff --git a/README.md b/README.md new file mode 100644 index 0000000..cf33033 --- /dev/null +++ b/README.md @@ -0,0 +1,61 @@ +# Site Healthcheck + +Internal WordPress plugin that walks a technician through a structured healthcheck. Companion to [`wp-site-recovery`](../wp-site-recovery/), which it expects to be installed as step 0. + +## Install + +```sh +ln -s ~/dev/wp-healthcheck /path/to/wp/wp-content/plugins/site-healthcheck +``` + +Then activate from *Plugins*. Settings appear under *Tools → Site Healthcheck*. + +## Use + +1. Open *Tools → Site Healthcheck*. +2. Confirm the recovery plugin status panel shows ✓ active. +3. Click **Start new healthcheck**. +4. Work through each step card. For each: choose a status (done / skipped / blocked / n/a) and add notes. +5. Click **Finish & generate report**. +6. Download the Markdown report or copy it to clipboard. + +One in-progress session per site at a time. Reports are not stored in the database (the plugin is meant to be uninstalled at the end of each engagement) — download them. + +## Adding, removing, reordering steps + +Each step is a single file under `includes/steps/` named `-.php`. + +```php +'; + echo '

Site Healthcheck

'; + + if (!$session) { + wph_render_start_panel(); + echo ''; + return; + } + + if ($session->is_finished()) { + wph_render_finished_panel($session); + echo ''; + return; + } + + wph_render_active_session($session); + echo ''; +} + +function wph_render_start_panel(): void { + ?> +
+

Start a healthcheck

+

This will create a new in-progress session for . One session per site at a time.

+
+ + + +
+
+ progress(); + $tech = get_userdata($session->technician_id()); + ?> +
+

+ Session: id()); ?> · + Started: started_at())); ?> · + Technician: display_name : '#' . $session->technician_id()); ?> · + / steps +

+

WP wp_version()); ?> · PHP php_version()); ?> · Site site_url()); ?>

+
+
+ + + +
+
+ + + +
+
+
+ all() as $step) { + wph_render_step_card($session, $step); + } +} + +function wph_render_step_card(WPH_Session $session, WPH_Step $step): void { + $state = $session->step_state($step->id()); + $status = $state['status']; + $notes = $state['notes']; + ?> +
+ id()); ?> + + +
+

title()); ?>

+ +
+ blurb()): ?> +

+ + sub_items()): ?> +
    + +
  • + +
+ + watch_outs()): ?> +
+ Watch out for: +
    + +
  • + +
+
+ + escalation())): ?> +
+ +

+ +

+

+ + +

+

+ + + Last saved ago + +

+
+ +
+

Healthcheck finished

+

Started started_at())); ?> · Finished finished_at())); ?>

+
+
+ + + +
+ +
+ + + +
+
+
+
+

Report preview

+
+
+ is_finished()) wp_die('No active session.'); + if (!WPH_Steps::instance()->get($step_id)) wp_die('Unknown step.'); + $status = isset($_POST['status']) ? sanitize_key((string) $_POST['status']) : WPH_Session::STATUS_NOT_STARTED; + $notes = isset($_POST['notes']) ? wp_unslash((string) $_POST['notes']) : ''; + $session->update_step($step_id, $status, $notes); + wp_safe_redirect(admin_url('tools.php?page=site-healthcheck#step-' . rawurlencode($step_id))); + exit; +} + +function wph_handle_finish(): void { + if (!current_user_can('manage_options')) wp_die('Forbidden'); + check_admin_referer('wph_finish'); + $session = WPH_Session::current(); + if (!$session) wp_die('No active session.'); + $session->finish(); + wp_safe_redirect(admin_url('tools.php?page=site-healthcheck')); + exit; +} + +function wph_handle_discard(): void { + if (!current_user_can('manage_options')) wp_die('Forbidden'); + check_admin_referer('wph_discard'); + WPH_Session::discard(); + wp_safe_redirect(admin_url('tools.php?page=site-healthcheck')); + exit; +} + +function wph_handle_download_report(): void { + if (!current_user_can('manage_options')) wp_die('Forbidden'); + check_admin_referer('wph_download_report'); + $session = WPH_Session::current(); + if (!$session) wp_die('No session.'); + $report = wph_build_markdown_report($session); + $host = parse_url(get_site_url(), PHP_URL_HOST) ?: 'site'; + $host = preg_replace('/[^a-z0-9.-]/i', '', (string) $host); + $stamp = date('Ymd', $session->started_at() ?: time()); + $filename = 'wph-report-' . $host . '-' . $stamp . '.md'; + nocache_headers(); + header('Content-Type: text/markdown; charset=UTF-8'); + header('Content-Disposition: attachment; filename="' . $filename . '"'); + echo $report; + exit; +} diff --git a/includes/class-wph-session.php b/includes/class-wph-session.php new file mode 100644 index 0000000..77a3ea6 --- /dev/null +++ b/includes/class-wph-session.php @@ -0,0 +1,105 @@ +data = $data; + } + + public static function current(): ?self { + $raw = get_option(WPH_OPT_SESSION); + if (!is_array($raw) || empty($raw['id'])) return null; + return new self($raw); + } + + public static function start(int $technician_id): self { + $data = [ + 'id' => uniqid('wph_', true), + 'started_at' => time(), + 'finished_at' => null, + 'technician_id'=> $technician_id, + 'site_url' => get_site_url(), + 'wp_version' => get_bloginfo('version'), + 'php_version' => PHP_VERSION, + 'steps' => [], // keyed by step id → ['status' => ..., 'notes' => ..., 'updated_at' => ...] + ]; + update_option(WPH_OPT_SESSION, $data, false); + return new self($data); + } + + public static function discard(): void { + delete_option(WPH_OPT_SESSION); + } + + public function id(): string { return (string) $this->data['id']; } + public function started_at(): int { return (int) $this->data['started_at']; } + public function finished_at(): ?int { return isset($this->data['finished_at']) ? (int) $this->data['finished_at'] : null; } + public function is_finished(): bool { return $this->finished_at() !== null; } + public function technician_id(): int { return (int) $this->data['technician_id']; } + public function site_url(): string { return (string) ($this->data['site_url'] ?? get_site_url()); } + public function wp_version(): string { return (string) ($this->data['wp_version'] ?? ''); } + public function php_version(): string { return (string) ($this->data['php_version'] ?? ''); } + public function data(): array { return $this->data; } + + public function step_state(string $step_id): array { + return $this->data['steps'][$step_id] ?? [ + 'status' => self::STATUS_NOT_STARTED, + 'notes' => '', + 'updated_at' => null, + ]; + } + + public function update_step(string $step_id, string $status, string $notes): void { + if (!in_array($status, self::VALID_STATUSES, true)) { + $status = self::STATUS_NOT_STARTED; + } + $this->data['steps'][$step_id] = [ + 'status' => $status, + 'notes' => $notes, + 'updated_at' => time(), + ]; + update_option(WPH_OPT_SESSION, $this->data, false); + } + + public function finish(): void { + $this->data['finished_at'] = time(); + update_option(WPH_OPT_SESSION, $this->data, false); + } + + public function progress(): array { + $steps = WPH_Steps::instance()->all(); + $total = count($steps); + $done = 0; + foreach ($steps as $step) { + $st = $this->step_state($step->id()); + if (in_array($st['status'], [self::STATUS_DONE, self::STATUS_SKIPPED, self::STATUS_NA], true)) { + $done++; + } + } + return ['done' => $done, 'total' => $total]; + } +} diff --git a/includes/class-wph-step.php b/includes/class-wph-step.php new file mode 100644 index 0000000..254ed82 --- /dev/null +++ b/includes/class-wph-step.php @@ -0,0 +1,45 @@ + + */ + public function autocheck(array $session_state): array { return []; } +} diff --git a/includes/class-wph-steps.php b/includes/class-wph-steps.php new file mode 100644 index 0000000..a6dd9f5 --- /dev/null +++ b/includes/class-wph-steps.php @@ -0,0 +1,55 @@ + */ + private array $steps = []; + + private bool $discovered = false; + + public static function instance(): WPH_Steps { + return self::$instance ??= new self(); + } + + public function discover(string $dir): void { + if ($this->discovered) return; + $files = glob(rtrim($dir, '/') . '/*.php') ?: []; + // Natural sort so 100-* comes after 20-* (not after 10-*). + natsort($files); + $files = array_values($files); + foreach ($files as $file) { + $obj = require $file; + if ($obj instanceof WPH_Step) { + $this->steps[$obj->id()] = $obj; + } + } + /** + * Filter the loaded steps. Return an array keyed by step id. + * To drop a step on a particular install: unset($steps['backup']). + */ + $this->steps = apply_filters('wph_steps', $this->steps); + $this->discovered = true; + } + + /** @return array */ + public function all(): array { + return $this->steps; + } + + public function get(string $id): ?WPH_Step { + return $this->steps[$id] ?? null; + } +} diff --git a/includes/recovery-bootstrap.php b/includes/recovery-bootstrap.php new file mode 100644 index 0000000..fb87e6e --- /dev/null +++ b/includes/recovery-bootstrap.php @@ -0,0 +1,58 @@ + +
+

Recovery plugin status

+ +

Site Recovery is installed and active. + Open recovery settings to copy the URL + password into the client record.

+ +

Site Recovery is installed but inactive. Activate it before starting work.

+ +

Site Recovery is not installed. Install it before starting the healthcheck — it's the safety net while we work on the site.

+

Phase-1: install manually via Plugins → Add New → Upload Plugin. A private update channel for one-click install lands in hc-5ix.27.

+ +
+ technician_id()); + $tech_name = $tech ? $tech->display_name : '#' . $session->technician_id(); + + $lines = []; + $lines[] = '# Site Healthcheck — ' . $session->site_url(); + $lines[] = ''; + $lines[] = '- **Started:** ' . date('Y-m-d H:i', $session->started_at()); + if ($session->is_finished()) { + $lines[] = '- **Finished:** ' . date('Y-m-d H:i', (int) $session->finished_at()); + $minutes = max(1, (int) round(((int) $session->finished_at() - $session->started_at()) / 60)); + $lines[] = '- **Duration:** ~' . $minutes . ' minute(s)'; + } + $lines[] = '- **Technician:** ' . $tech_name; + $lines[] = '- **Site:** ' . $session->site_url(); + $lines[] = '- **WordPress:** ' . $session->wp_version(); + $lines[] = '- **PHP:** ' . $session->php_version(); + $lines[] = '- **Session ID:** ' . $session->id(); + $lines[] = ''; + + // Summary table + $lines[] = '## Summary'; + $lines[] = ''; + $lines[] = '| Step | Status |'; + $lines[] = '|---|---|'; + foreach (WPH_Steps::instance()->all() as $step) { + $state = $session->step_state($step->id()); + $lines[] = '| ' . $step->title() . ' | ' . wph_status_label($state['status']) . ' |'; + } + $lines[] = ''; + + // Per-step detail + $lines[] = '## Detail'; + $lines[] = ''; + foreach (WPH_Steps::instance()->all() as $step) { + $state = $session->step_state($step->id()); + $lines[] = '### ' . $step->title(); + $lines[] = ''; + $lines[] = '_Status: ' . wph_status_label($state['status']) . '_'; + if ($state['updated_at']) { + $lines[] = '_Saved: ' . date('Y-m-d H:i', $state['updated_at']) . '_'; + } + if ($blurb = $step->blurb()) { + $lines[] = ''; + $lines[] = $blurb; + } + if ($items = $step->sub_items()) { + $lines[] = ''; + foreach ($items as $item) { + $lines[] = '- [ ] ' . $item; + } + } + if (!empty($state['notes'])) { + $lines[] = ''; + $lines[] = '**Notes:**'; + $lines[] = ''; + foreach (preg_split('/\R/', (string) $state['notes']) as $nl) { + $lines[] = '> ' . $nl; + } + } + if ($state['status'] === WPH_Session::STATUS_BLOCKED && ($esc = $step->escalation())) { + $lines[] = ''; + $lines[] = '> ⚠ **Escalation:** ' . $esc; + } + $lines[] = ''; + } + + $lines[] = '---'; + $lines[] = '_Generated by Site Healthcheck plugin v' . WPH_VERSION . '_'; + + return implode("\n", $lines) . "\n"; +} + +function wph_status_label(string $status): string { + switch ($status) { + case WPH_Session::STATUS_DONE: return '✅ Done'; + case WPH_Session::STATUS_SKIPPED: return '⏭ Skipped'; + case WPH_Session::STATUS_BLOCKED: return '🛑 Blocked'; + case WPH_Session::STATUS_NA: return '— N/A'; + case WPH_Session::STATUS_NOT_STARTED: + default: return '◻ Not started'; + } +} diff --git a/includes/steps/00-before.php b/includes/steps/00-before.php new file mode 100644 index 0000000..4356f8c --- /dev/null +++ b/includes/steps/00-before.php @@ -0,0 +1,19 @@ +discover(WPH_PLUGIN_DIR . 'includes/steps/'); +}); diff --git a/steps.md b/steps.md new file mode 100644 index 0000000..2079c51 --- /dev/null +++ b/steps.md @@ -0,0 +1,168 @@ +## WordPress Site Healthcheck — Technician Guide +### Before You Start + +- Confirm you have admin access to the WordPress dashboard and hosting control panel +- Check the client record for any known issues, recent changes, or flags from the previous healthcheck +- Note the current WordPress version, PHP version, and active theme before touching anything + +- Install our recovery plugin. This plugin should also step you through what's needed for the healthcheck as well. +- Make a note of the recovery path in case there is a problem + + +--- + +### Step 1 — Take a Full Backup + +Before any work takes place, take a complete backup manually. Do not rely on the most recent automated backup. + +- Back up both the database and all files (wp-content, wp-config.php, .htaccess) +- Confirm the backup has completed and is accessible/downloadable +- Note the backup location and timestamp in the client record + +**If the backup fails or cannot be confirmed, stop. Do not proceed until you have a verified backup.** + +--- + +### Step 2 — Environment Check + +Before touching updates, review the hosting environment: + +- **PHP version** — check against WordPress requirements and plugin compatibility. Flag if below 8.1. Note if EOL. +- **Disk usage** — flag if over 80% used +- **Error logs** — check the server error log and WordPress debug log if enabled. Note any recurring errors, 500s, or deprecated function warnings +- **wp-config.php** — confirm WP_DEBUG is off on production +- **File permissions** — spot check wp-config.php (should be 640 or 600), wp-content (755), uploads (755) + +--- + +### Step 3 — WordPress Core Update + +- Check current version against latest stable release +- If an update is available, apply it +- After update, load the site front-end and wp-admin and confirm both are functioning +- Check the database upgrade prompt — if WordPress prompts to upgrade the database, run it +- Note the version you updated from and to + +**Watch out for:** white screen of death post-update, admin redirect loops, missing admin menu items — these usually indicate a theme or plugin conflict with the new core version. + +--- + +### Step 4 — Plugin Updates + +- Go to Dashboard → Updates and review all pending plugin updates +- Before updating, note which plugins have updates and what versions they are moving to +- Update plugins one at a time if the site is complex or has many interdependencies; batch update is acceptable for straightforward sites +- After each update (or after a batch), check the front-end and any key functional areas (forms, checkout, membership, etc.) +- Check for any plugins that have been deactivated but not deleted — flag these to the client + +**Watch out for:** +- WooCommerce updates — always treat these as high-risk, test checkout flow afterwards +- Page builder updates (Elementor, Divi, Beaver Builder) — can affect layout rendering +- Security plugin updates — confirm they reactivate and are still reporting clean +- Plugins that haven't been updated by their developer in over 12 months — flag as a risk +- Plugins showing "Update unavailable" or removed from the WordPress repository — flag immediately, these can indicate abandoned or compromised plugins + +--- + +### Step 5 — Theme Updates + +- Update the active theme if an update is available +- If a child theme is in use (correct practice), the parent theme can be updated safely — confirm child theme is active +- If no child theme is in use and the parent theme has been customised directly, do not update without flagging to the client first — the update will overwrite customisations +- Update inactive themes only if they are legitimate fallback themes (e.g. a default Twenty* theme). Unused themes that serve no purpose should be flagged for removal + +**Watch out for:** layout changes post-theme update, broken header/footer, missing custom fonts or colours — indicates customisation was done directly in the parent theme. + +--- + +### Step 6 — Visual and Functional Check + +Do a manual walkthrough of the site: + +- **Homepage** — load and visually inspect. Check for broken images, layout issues, console errors (open browser dev tools) +- **Navigation** — click through the main menu. Confirm all links resolve correctly, no 404s on primary pages +- **Key pages** — About, Contact, Services or equivalent. Check content renders correctly +- **Contact form** — submit a test entry and confirm it delivers (check spam folder if no delivery). Note which form plugin is in use +- **If WooCommerce** — check shop page loads, a product page loads, add to basket works. Do not need to complete a full test purchase every time unless flagged +- **If membership/login** — confirm login page loads and (if test credentials available) login works +- **Mobile view** — check the homepage and one internal page on a mobile viewport in browser dev tools +- **HTTPS** — confirm the padlock is showing and there are no mixed content warnings +- **Redirects** — confirm www/non-www and HTTP/HTTPS are redirecting correctly to the canonical URL + +--- + +### Step 7 — Performance Check + +- Run a quick PageSpeed Insights check on the homepage +- Note the scores (mobile and desktop) in the client record +- Flag if mobile score has dropped significantly since last check (more than 10 points) +- Check that caching is active — if using a caching plugin, confirm it is enabled and not throwing errors +- Check image sizes on the homepage — flag if uncompressed images over 500KB are being served + +--- + +### Step 8 — Security Check + +- Confirm the SSL certificate is valid and not expiring within 30 days — flag if so +- Check the WordPress user list — flag any unfamiliar admin accounts +- Check for any recently modified core files if you have file change monitoring in place +- Confirm the login URL is not the default /wp-admin if security hardening was previously applied +- If a security plugin is active, review its dashboard for any flagged issues +- Check that xmlrpc.php is disabled or restricted if not in use + +--- + +### Step 9 — Database + +- Run a database optimisation (via WP-CLI: `wp db optimize`, or via a plugin such as WP-Optimize) +- Check for and remove any spam comments if comment moderation hasn't been keeping up +- Check post revisions — if excessive (thousands), note for client discussion on whether a revision limit should be set + +--- + +### Step 10 — Uptime and Availability + +- Check uptime monitoring logs if available — note any downtime incidents since the last healthcheck and flag to client if significant +- Confirm the site is resolving correctly on both www and non-www + +--- + +### Step 11 — Small Fixes + +Address any small issues found during the check that fall within the ~15 minute threshold: + +- Broken internal links on key pages +- Missing alt text on homepage images +- Obvious content errors noticed in passing (broken shortcodes, missing widgets) +- Reactivating a deactivated-but-needed plugin +- Clearing accumulated spam comments + +For anything that will take longer, write it up as a separate item to quote for. + +--- + +### Step 12 — Wrap Up and Document + +- Log everything done in the client record: versions before and after, any issues found, any fixes applied, anything flagged for follow-up +- Note the date, time taken, and technician +- If using ManageWP or WP Umbrella, generate the client report and review it before sending — make sure it accurately reflects what was done +- Send the client report or file it according to your process +- If anything was flagged that needs a separate quote or client decision, send that communication now rather than leaving it + +--- + +### Quick Reference — When to Stop and Escalate + +| Situation | Action | +|---|---| +| Backup cannot be confirmed | Stop. Do not proceed. | +| Site is down before you start | Diagnose and resolve before running updates | +| Core update causes white screen | Restore backup immediately | +| Plugin removed from WP repository | Flag to client before removing | +| Unfamiliar admin user found | Flag to client immediately, do not delete without instruction | +| SSL expiring within 30 days | Escalate for renewal before next healthcheck | +| PHP version EOL | Raise with client as a separate piece of work | + +--- + +*This is intended as a repeatable checklist rather than a rigid script — experienced technicians will move through familiar sites faster, but the sequence and the stop/escalate conditions should be consistent regardless of who runs the check.*