Phase 1 MVP: drop-in step registry + session + Markdown report
Plugin skeleton, drop-in step registry, option-backed session, single-page checklist UI, downloadable Markdown report. Steps live as one file each under includes/steps/ — adding/removing one is a single file change. Step IDs are stable strings so renaming files preserves session data. Architecture (hc-5ix.3): WPH_Steps singleton globs includes/steps/*.php, natsort-orders by filename, requires each file (which returns a WPH_Step instance), then applies a 'wph_steps' filter so installs can drop steps. Session (hc-5ix.4): option-backed (per decision — plugin is installed per-engagement, so DB-resident history would be lost on uninstall). Single in-progress session per site; finished sessions render a report that the user downloads/copies. Recovery bootstrap (hc-5ix.1): detects whether wp-site-recovery is installed + active, surfaces state on the start panel and in every active session. Manual install for now; private update channel deferred to hc-5ix.27. Smoke-tested on testsite: registry discovery (13 steps in correct order), start → update_step → progress count → finish → 8KB Markdown report → discard cycle.
This commit is contained in:
28
.beads/issues.jsonl
Normal file
28
.beads/issues.jsonl
Normal file
@@ -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}
|
||||||
61
README.md
Normal file
61
README.md
Normal file
@@ -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 `<order>-<slug>.php`.
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
// includes/steps/45-staging.php
|
||||||
|
return new class extends WPH_Step {
|
||||||
|
public function id(): string { return 'staging'; }
|
||||||
|
public function title(): string { return 'Step 4.5 — Verify Staging Sync'; }
|
||||||
|
public function sub_items(): array {
|
||||||
|
return ['Re-deploy from production', 'Run smoke tests'];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Add:** drop a new file.
|
||||||
|
- **Remove:** delete the file.
|
||||||
|
- **Reorder:** rename the numeric prefix (steps are loaded in `natsort` order).
|
||||||
|
- **Conditionally drop on one install:** use the `wph_steps` filter to `unset` the step by id.
|
||||||
|
|
||||||
|
Stable string IDs (returned by `id()`) are what's stored in session data, so renaming a file does not break in-progress sessions as long as the id stays the same.
|
||||||
|
|
||||||
|
## Phase roadmap
|
||||||
|
|
||||||
|
Phase 1 (this) is a checklist + Markdown report. Phase 3 will add per-step automation — backup detection, env auto-collect, plugin update intelligence, PageSpeed Insights, SSL checks, etc. See beads epic `hc-5ix` for the full plan.
|
||||||
|
|
||||||
|
## Distribution
|
||||||
|
|
||||||
|
Internal/agency tool — not a WP.org plugin (decision `hc-5ix.26`). Distributed as a built ZIP. A self-hosted update channel is `hc-5ix.27` on the phase-3 backlog.
|
||||||
|
|
||||||
|
## Tracking
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cd ~/dev/wp-healthcheck
|
||||||
|
bd list --label phase-1 # MVP work
|
||||||
|
bd list --label phase-3 # automation backlog
|
||||||
|
```
|
||||||
275
includes/admin-page.php
Normal file
275
includes/admin-page.php
Normal file
@@ -0,0 +1,275 @@
|
|||||||
|
<?php
|
||||||
|
if (!defined('ABSPATH')) exit;
|
||||||
|
|
||||||
|
add_action('admin_menu', 'wph_register_menu');
|
||||||
|
add_action('admin_post_wph_start', 'wph_handle_start');
|
||||||
|
add_action('admin_post_wph_save_step', 'wph_handle_save_step');
|
||||||
|
add_action('admin_post_wph_finish', 'wph_handle_finish');
|
||||||
|
add_action('admin_post_wph_discard', 'wph_handle_discard');
|
||||||
|
add_action('admin_post_wph_download_report', 'wph_handle_download_report');
|
||||||
|
add_action('admin_enqueue_scripts', 'wph_enqueue_assets');
|
||||||
|
|
||||||
|
function wph_register_menu(): void {
|
||||||
|
add_management_page(
|
||||||
|
'Site Healthcheck',
|
||||||
|
'Site Healthcheck',
|
||||||
|
'manage_options',
|
||||||
|
'site-healthcheck',
|
||||||
|
'wph_render_admin_page'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function wph_enqueue_assets($hook): void {
|
||||||
|
if ($hook !== 'tools_page_site-healthcheck') return;
|
||||||
|
wp_register_style('wph-admin', false);
|
||||||
|
wp_enqueue_style('wph-admin');
|
||||||
|
wp_add_inline_style('wph-admin', wph_inline_css());
|
||||||
|
}
|
||||||
|
|
||||||
|
function wph_inline_css(): string {
|
||||||
|
return '
|
||||||
|
.wph-card { background:#fff; border:1px solid #c3c4c7; border-radius:6px; padding:1rem 1.25rem; margin-bottom:1rem; }
|
||||||
|
.wph-card h2 { margin-top:0; }
|
||||||
|
.wph-step-status { display:inline-block; padding:.1rem .55rem; border-radius:10px; font-size:11px; font-weight:600; text-transform:uppercase; letter-spacing:.04em; }
|
||||||
|
.wph-status-not_started { background:#f0f0f1; color:#646970; }
|
||||||
|
.wph-status-done { background:#def7e3; color:#155724; }
|
||||||
|
.wph-status-skipped { background:#fff3cd; color:#856404; }
|
||||||
|
.wph-status-blocked { background:#fbeae8; color:#721c24; }
|
||||||
|
.wph-status-n_a { background:#e2e3e5; color:#41464b; }
|
||||||
|
.wph-step { padding:1rem 1.25rem; border:1px solid #dcdcde; border-radius:6px; margin-bottom:.75rem; background:#fff; }
|
||||||
|
.wph-step header { display:flex; justify-content:space-between; align-items:center; gap:1rem; margin-bottom:.5rem; }
|
||||||
|
.wph-step header h2 { margin:0; font-size:1.1rem; }
|
||||||
|
.wph-sub-items { margin:.5rem 0 .75rem 1.25rem; padding:0; }
|
||||||
|
.wph-sub-items li { margin:.15rem 0; }
|
||||||
|
.wph-watch-outs { background:#fff8e1; border-left:3px solid #f5b800; padding:.4rem .8rem; margin:.5rem 0; font-size:.92em; }
|
||||||
|
.wph-watch-outs strong { display:block; margin-bottom:.2rem; }
|
||||||
|
.wph-escalation { background:#fbeae8; border-left:3px solid #c0392b; padding:.4rem .8rem; margin:.5rem 0; font-weight:500; }
|
||||||
|
.wph-step textarea { width:100%; min-height:60px; }
|
||||||
|
.wph-progress { font-weight:600; }
|
||||||
|
.wph-ok { color:#155724; }
|
||||||
|
.wph-warn { color:#856404; }
|
||||||
|
.wph-bad { color:#721c24; }
|
||||||
|
.wph-bootstrap-panel { padding:.6rem 1rem; background:#f6f7f7; border:1px solid #dcdcde; border-radius:6px; margin-bottom:.75rem; }
|
||||||
|
.wph-bootstrap-panel h3 { margin:0 0 .35rem; font-size:1rem; }
|
||||||
|
.wph-actions { display:flex; gap:.5rem; align-items:center; margin-top:.4rem; }
|
||||||
|
';
|
||||||
|
}
|
||||||
|
|
||||||
|
function wph_render_admin_page(): void {
|
||||||
|
if (!current_user_can('manage_options')) wp_die('Forbidden');
|
||||||
|
|
||||||
|
$session = WPH_Session::current();
|
||||||
|
|
||||||
|
echo '<div class="wrap">';
|
||||||
|
echo '<h1>Site Healthcheck</h1>';
|
||||||
|
|
||||||
|
if (!$session) {
|
||||||
|
wph_render_start_panel();
|
||||||
|
echo '</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($session->is_finished()) {
|
||||||
|
wph_render_finished_panel($session);
|
||||||
|
echo '</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
wph_render_active_session($session);
|
||||||
|
echo '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
function wph_render_start_panel(): void {
|
||||||
|
?>
|
||||||
|
<div class="wph-card">
|
||||||
|
<h2>Start a healthcheck</h2>
|
||||||
|
<p>This will create a new in-progress session for <code><?php echo esc_html(get_site_url()); ?></code>. One session per site at a time.</p>
|
||||||
|
<form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>">
|
||||||
|
<?php wp_nonce_field('wph_start'); ?>
|
||||||
|
<input type="hidden" name="action" value="wph_start">
|
||||||
|
<button class="button button-primary">Start new healthcheck</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
WPH_Recovery_Bootstrap::render_status();
|
||||||
|
}
|
||||||
|
|
||||||
|
function wph_render_active_session(WPH_Session $session): void {
|
||||||
|
$progress = $session->progress();
|
||||||
|
$tech = get_userdata($session->technician_id());
|
||||||
|
?>
|
||||||
|
<div class="wph-card">
|
||||||
|
<p>
|
||||||
|
<strong>Session:</strong> <code><?php echo esc_html($session->id()); ?></code> ·
|
||||||
|
<strong>Started:</strong> <?php echo esc_html(date('Y-m-d H:i', $session->started_at())); ?> ·
|
||||||
|
<strong>Technician:</strong> <?php echo esc_html($tech ? $tech->display_name : '#' . $session->technician_id()); ?> ·
|
||||||
|
<span class="wph-progress"><?php echo (int) $progress['done']; ?> / <?php echo (int) $progress['total']; ?> steps</span>
|
||||||
|
</p>
|
||||||
|
<p>WP <code><?php echo esc_html($session->wp_version()); ?></code> · PHP <code><?php echo esc_html($session->php_version()); ?></code> · Site <code><?php echo esc_html($session->site_url()); ?></code></p>
|
||||||
|
<div class="wph-actions">
|
||||||
|
<form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>" onsubmit="return confirm('Mark this healthcheck as finished?');" style="display:inline">
|
||||||
|
<?php wp_nonce_field('wph_finish'); ?>
|
||||||
|
<input type="hidden" name="action" value="wph_finish">
|
||||||
|
<button class="button button-primary">Finish & generate report</button>
|
||||||
|
</form>
|
||||||
|
<form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>" onsubmit="return confirm('Discard this in-progress session? All notes will be lost.');" style="display:inline">
|
||||||
|
<?php wp_nonce_field('wph_discard'); ?>
|
||||||
|
<input type="hidden" name="action" value="wph_discard">
|
||||||
|
<button class="button button-link-delete">Discard</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
|
||||||
|
WPH_Recovery_Bootstrap::render_status();
|
||||||
|
|
||||||
|
foreach (WPH_Steps::instance()->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'];
|
||||||
|
?>
|
||||||
|
<form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>" class="wph-step" id="step-<?php echo esc_attr($step->id()); ?>">
|
||||||
|
<?php wp_nonce_field('wph_save_step_' . $step->id()); ?>
|
||||||
|
<input type="hidden" name="action" value="wph_save_step">
|
||||||
|
<input type="hidden" name="step" value="<?php echo esc_attr($step->id()); ?>">
|
||||||
|
<header>
|
||||||
|
<h2><?php echo esc_html($step->title()); ?></h2>
|
||||||
|
<span class="wph-step-status wph-status-<?php echo esc_attr($status); ?>"><?php echo esc_html(str_replace('_', ' ', $status)); ?></span>
|
||||||
|
</header>
|
||||||
|
<?php if ($blurb = $step->blurb()): ?>
|
||||||
|
<p><?php echo esc_html($blurb); ?></p>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if ($items = $step->sub_items()): ?>
|
||||||
|
<ul class="wph-sub-items">
|
||||||
|
<?php foreach ($items as $item): ?>
|
||||||
|
<li><?php echo esc_html($item); ?></li>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</ul>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if ($watch = $step->watch_outs()): ?>
|
||||||
|
<div class="wph-watch-outs">
|
||||||
|
<strong>Watch out for:</strong>
|
||||||
|
<ul style="margin:.2rem 0 0 1rem;">
|
||||||
|
<?php foreach ($watch as $w): ?>
|
||||||
|
<li><?php echo esc_html($w); ?></li>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if ($status === WPH_Session::STATUS_BLOCKED && ($esc = $step->escalation())): ?>
|
||||||
|
<div class="wph-escalation"><?php echo esc_html($esc); ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<p>
|
||||||
|
<label>
|
||||||
|
<strong>Status:</strong>
|
||||||
|
<select name="status">
|
||||||
|
<?php foreach (WPH_Session::VALID_STATUSES as $s): ?>
|
||||||
|
<option value="<?php echo esc_attr($s); ?>" <?php selected($status, $s); ?>><?php echo esc_html(str_replace('_', ' ', $s)); ?></option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<label><strong>Notes:</strong></label>
|
||||||
|
<textarea name="notes" placeholder="What did you check, find, fix, or flag?"><?php echo esc_textarea($notes); ?></textarea>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<button class="button">Save step</button>
|
||||||
|
<?php if ($state['updated_at']): ?>
|
||||||
|
<span class="description">Last saved <?php echo esc_html(human_time_diff($state['updated_at'], time())); ?> ago</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</p>
|
||||||
|
</form>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
function wph_render_finished_panel(WPH_Session $session): void {
|
||||||
|
$report = wph_build_markdown_report($session);
|
||||||
|
?>
|
||||||
|
<div class="wph-card">
|
||||||
|
<h2>Healthcheck finished</h2>
|
||||||
|
<p>Started <?php echo esc_html(date('Y-m-d H:i', $session->started_at())); ?> · Finished <?php echo esc_html(date('Y-m-d H:i', (int) $session->finished_at())); ?></p>
|
||||||
|
<div class="wph-actions">
|
||||||
|
<form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>">
|
||||||
|
<?php wp_nonce_field('wph_download_report'); ?>
|
||||||
|
<input type="hidden" name="action" value="wph_download_report">
|
||||||
|
<button class="button button-primary">Download Markdown report</button>
|
||||||
|
</form>
|
||||||
|
<button class="button" onclick="navigator.clipboard.writeText(document.getElementById('wph-report-md').textContent);this.textContent='Copied!';setTimeout(()=>this.textContent='Copy to clipboard',2000)">Copy to clipboard</button>
|
||||||
|
<form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>" onsubmit="return confirm('Discard this finished session?');" style="display:inline">
|
||||||
|
<?php wp_nonce_field('wph_discard'); ?>
|
||||||
|
<input type="hidden" name="action" value="wph_discard">
|
||||||
|
<button class="button button-link-delete">Discard & start a new one</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="wph-card">
|
||||||
|
<h2>Report preview</h2>
|
||||||
|
<pre id="wph-report-md" style="white-space:pre-wrap;background:#f6f7f7;padding:1rem;border-radius:6px;max-height:30em;overflow:auto"><?php echo esc_html($report); ?></pre>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Handlers ----------------------------------------------------------------
|
||||||
|
|
||||||
|
function wph_handle_start(): void {
|
||||||
|
if (!current_user_can('manage_options')) wp_die('Forbidden');
|
||||||
|
check_admin_referer('wph_start');
|
||||||
|
WPH_Session::start(get_current_user_id());
|
||||||
|
wp_safe_redirect(admin_url('tools.php?page=site-healthcheck'));
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
function wph_handle_save_step(): void {
|
||||||
|
if (!current_user_can('manage_options')) wp_die('Forbidden');
|
||||||
|
$step_id = isset($_POST['step']) ? sanitize_key((string) $_POST['step']) : '';
|
||||||
|
check_admin_referer('wph_save_step_' . $step_id);
|
||||||
|
$session = WPH_Session::current();
|
||||||
|
if (!$session || $session->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;
|
||||||
|
}
|
||||||
105
includes/class-wph-session.php
Normal file
105
includes/class-wph-session.php
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
<?php
|
||||||
|
if (!defined('ABSPATH')) exit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Healthcheck session. Option-backed (single in-progress session per site).
|
||||||
|
*
|
||||||
|
* Per beads decision hc-5ix.4: option, not CPT. The plugin is installed per
|
||||||
|
* engagement, so per-site history living in the DB would die on uninstall.
|
||||||
|
* Reports are exported as Markdown instead — see report.php.
|
||||||
|
*/
|
||||||
|
final class WPH_Session {
|
||||||
|
|
||||||
|
public const STATUS_NOT_STARTED = 'not_started';
|
||||||
|
public const STATUS_DONE = 'done';
|
||||||
|
public const STATUS_SKIPPED = 'skipped';
|
||||||
|
public const STATUS_BLOCKED = 'blocked';
|
||||||
|
public const STATUS_NA = 'n_a';
|
||||||
|
|
||||||
|
public const VALID_STATUSES = [
|
||||||
|
self::STATUS_NOT_STARTED,
|
||||||
|
self::STATUS_DONE,
|
||||||
|
self::STATUS_SKIPPED,
|
||||||
|
self::STATUS_BLOCKED,
|
||||||
|
self::STATUS_NA,
|
||||||
|
];
|
||||||
|
|
||||||
|
private array $data;
|
||||||
|
|
||||||
|
private function __construct(array $data) {
|
||||||
|
$this->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];
|
||||||
|
}
|
||||||
|
}
|
||||||
45
includes/class-wph-step.php
Normal file
45
includes/class-wph-step.php
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
<?php
|
||||||
|
if (!defined('ABSPATH')) exit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base class for a healthcheck step.
|
||||||
|
*
|
||||||
|
* Each step lives in its own file under includes/steps/ with a numeric prefix
|
||||||
|
* (e.g. 10-backup.php). The file returns an instance of a subclass of WPH_Step.
|
||||||
|
* Drop a new file in that directory and it shows up; delete a file and it
|
||||||
|
* disappears. The numeric prefix controls order so re-ordering is a rename.
|
||||||
|
*
|
||||||
|
* Stable string IDs (returned by id()) are stored in the session, so renaming
|
||||||
|
* the file does NOT lose data as long as id() stays the same.
|
||||||
|
*/
|
||||||
|
abstract class WPH_Step {
|
||||||
|
|
||||||
|
/** Stable identifier (lowercase slug). NEVER change once shipped. */
|
||||||
|
abstract public function id(): string;
|
||||||
|
|
||||||
|
/** Human-readable title shown in the UI and report. */
|
||||||
|
abstract public function title(): string;
|
||||||
|
|
||||||
|
/** Intro paragraph shown under the title (plain text or simple inline HTML). */
|
||||||
|
public function blurb(): string { return ''; }
|
||||||
|
|
||||||
|
/** Sub-items the technician should tick through (plain strings). */
|
||||||
|
public function sub_items(): array { return []; }
|
||||||
|
|
||||||
|
/** "Watch out for" callouts (plain strings). */
|
||||||
|
public function watch_outs(): array { return []; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If this step has a stop/escalate condition (e.g. backup failed → stop),
|
||||||
|
* return the guidance string. Rendered as a banner when status=blocked.
|
||||||
|
*/
|
||||||
|
public function escalation(): ?string { return null; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Phase 3 hook — return structured findings (php version, plugin update
|
||||||
|
* intel, etc.) for the technician to verify. Phase 1 returns nothing.
|
||||||
|
*
|
||||||
|
* @return array<string,mixed>
|
||||||
|
*/
|
||||||
|
public function autocheck(array $session_state): array { return []; }
|
||||||
|
}
|
||||||
55
includes/class-wph-steps.php
Normal file
55
includes/class-wph-steps.php
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
<?php
|
||||||
|
if (!defined('ABSPATH')) exit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Step registry. Singleton.
|
||||||
|
*
|
||||||
|
* discover() globs a directory for *.php files in sort order and requires each.
|
||||||
|
* Each file MUST return a WPH_Step instance. The numeric filename prefix
|
||||||
|
* (00, 10, 20...) controls the visual order.
|
||||||
|
*
|
||||||
|
* After discovery, the 'wph_steps' filter lets installation-specific code
|
||||||
|
* remove/replace steps (return the array keyed by step id).
|
||||||
|
*/
|
||||||
|
final class WPH_Steps {
|
||||||
|
|
||||||
|
private static ?WPH_Steps $instance = null;
|
||||||
|
|
||||||
|
/** @var array<string,WPH_Step> */
|
||||||
|
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<string,WPH_Step> */
|
||||||
|
public function all(): array {
|
||||||
|
return $this->steps;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get(string $id): ?WPH_Step {
|
||||||
|
return $this->steps[$id] ?? null;
|
||||||
|
}
|
||||||
|
}
|
||||||
58
includes/recovery-bootstrap.php
Normal file
58
includes/recovery-bootstrap.php
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
<?php
|
||||||
|
if (!defined('ABSPATH')) exit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Step-0 bootstrap: ensure wp-site-recovery is installed + active before the
|
||||||
|
* stepper runs (bead hc-5ix.1, references decision hc-5ix.26).
|
||||||
|
*
|
||||||
|
* Detection is by plugin folder slug, not by main-file path, because the slug
|
||||||
|
* is stable across releases. If we ever publish a private update channel for
|
||||||
|
* the recovery plugin, this is also where we'd offer "fetch latest".
|
||||||
|
*/
|
||||||
|
final class WPH_Recovery_Bootstrap {
|
||||||
|
|
||||||
|
public const RECOVERY_SLUG = 'site-recovery';
|
||||||
|
public const RECOVERY_MAIN = 'site-recovery/site-recovery.php';
|
||||||
|
|
||||||
|
public static function is_installed(): bool {
|
||||||
|
if (!function_exists('get_plugins')) {
|
||||||
|
require_once ABSPATH . 'wp-admin/includes/plugin.php';
|
||||||
|
}
|
||||||
|
return array_key_exists(self::RECOVERY_MAIN, get_plugins());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function is_active(): bool {
|
||||||
|
if (!function_exists('is_plugin_active')) {
|
||||||
|
require_once ABSPATH . 'wp-admin/includes/plugin.php';
|
||||||
|
}
|
||||||
|
return is_plugin_active(self::RECOVERY_MAIN);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function recovery_admin_url(): string {
|
||||||
|
return admin_url('tools.php?page=site-recovery');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render a status panel that the admin page calls from the "Before you start"
|
||||||
|
* section. Phase-1: report state + link to manage. Phase-3 may add a
|
||||||
|
* one-click installer from a private URL.
|
||||||
|
*/
|
||||||
|
public static function render_status(): void {
|
||||||
|
$installed = self::is_installed();
|
||||||
|
$active = $installed && self::is_active();
|
||||||
|
?>
|
||||||
|
<div class="wph-bootstrap-panel">
|
||||||
|
<h3>Recovery plugin status</h3>
|
||||||
|
<?php if ($active): ?>
|
||||||
|
<p class="wph-ok">✓ <strong>Site Recovery</strong> is installed and active.
|
||||||
|
<a href="<?php echo esc_url(self::recovery_admin_url()); ?>">Open recovery settings</a> to copy the URL + password into the client record.</p>
|
||||||
|
<?php elseif ($installed): ?>
|
||||||
|
<p class="wph-warn">⚠ <strong>Site Recovery</strong> is installed but inactive. <a href="<?php echo esc_url(admin_url('plugins.php')); ?>">Activate it</a> before starting work.</p>
|
||||||
|
<?php else: ?>
|
||||||
|
<p class="wph-bad">✗ <strong>Site Recovery</strong> is not installed. Install it before starting the healthcheck — it's the safety net while we work on the site.</p>
|
||||||
|
<p class="description">Phase-1: install manually via <em>Plugins → Add New → Upload Plugin</em>. A private update channel for one-click install lands in hc-5ix.27.</p>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
}
|
||||||
91
includes/report.php
Normal file
91
includes/report.php
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
<?php
|
||||||
|
if (!defined('ABSPATH')) exit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build a Markdown report for a healthcheck session.
|
||||||
|
* Per bead hc-5ix.7: pure Markdown, downloadable. No DB-side report storage
|
||||||
|
* (plugin may be uninstalled at the end of the engagement).
|
||||||
|
*/
|
||||||
|
function wph_build_markdown_report(WPH_Session $session): string {
|
||||||
|
$tech = get_userdata($session->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';
|
||||||
|
}
|
||||||
|
}
|
||||||
19
includes/steps/00-before.php
Normal file
19
includes/steps/00-before.php
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
if (!defined('ABSPATH')) exit;
|
||||||
|
|
||||||
|
return new class extends WPH_Step {
|
||||||
|
public function id(): string { return 'before'; }
|
||||||
|
public function title(): string { return 'Before You Start'; }
|
||||||
|
public function blurb(): string {
|
||||||
|
return 'Confirm prerequisites before touching anything.';
|
||||||
|
}
|
||||||
|
public function sub_items(): array {
|
||||||
|
return [
|
||||||
|
'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 (Site Recovery) — done via the button below',
|
||||||
|
'Make a note of the recovery path in case there is a problem',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
};
|
||||||
20
includes/steps/10-backup.php
Normal file
20
includes/steps/10-backup.php
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<?php
|
||||||
|
if (!defined('ABSPATH')) exit;
|
||||||
|
|
||||||
|
return new class extends WPH_Step {
|
||||||
|
public function id(): string { return 'backup'; }
|
||||||
|
public function title(): string { return 'Step 1 — Take a Full Backup'; }
|
||||||
|
public function blurb(): string {
|
||||||
|
return 'Before any work takes place, take a complete backup manually. Do not rely on the most recent automated backup.';
|
||||||
|
}
|
||||||
|
public function sub_items(): array {
|
||||||
|
return [
|
||||||
|
'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',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
public function escalation(): ?string {
|
||||||
|
return 'If the backup fails or cannot be confirmed, stop. Do not proceed until you have a verified backup.';
|
||||||
|
}
|
||||||
|
};
|
||||||
13
includes/steps/100-uptime.php
Normal file
13
includes/steps/100-uptime.php
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
if (!defined('ABSPATH')) exit;
|
||||||
|
|
||||||
|
return new class extends WPH_Step {
|
||||||
|
public function id(): string { return 'uptime'; }
|
||||||
|
public function title(): string { return 'Step 10 — Uptime and Availability'; }
|
||||||
|
public function sub_items(): array {
|
||||||
|
return [
|
||||||
|
'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',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
};
|
||||||
19
includes/steps/110-small-fixes.php
Normal file
19
includes/steps/110-small-fixes.php
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
if (!defined('ABSPATH')) exit;
|
||||||
|
|
||||||
|
return new class extends WPH_Step {
|
||||||
|
public function id(): string { return 'small_fixes'; }
|
||||||
|
public function title(): string { return 'Step 11 — Small Fixes'; }
|
||||||
|
public function blurb(): string {
|
||||||
|
return 'Address any small issues found during the check that fall within the ~15 minute threshold. Anything longer goes on the quote list.';
|
||||||
|
}
|
||||||
|
public function sub_items(): array {
|
||||||
|
return [
|
||||||
|
'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',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
};
|
||||||
16
includes/steps/120-wrap-up.php
Normal file
16
includes/steps/120-wrap-up.php
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<?php
|
||||||
|
if (!defined('ABSPATH')) exit;
|
||||||
|
|
||||||
|
return new class extends WPH_Step {
|
||||||
|
public function id(): string { return 'wrap_up'; }
|
||||||
|
public function title(): string { return 'Step 12 — Wrap Up and Document'; }
|
||||||
|
public function sub_items(): array {
|
||||||
|
return [
|
||||||
|
'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',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
};
|
||||||
19
includes/steps/20-environment.php
Normal file
19
includes/steps/20-environment.php
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
if (!defined('ABSPATH')) exit;
|
||||||
|
|
||||||
|
return new class extends WPH_Step {
|
||||||
|
public function id(): string { return 'environment'; }
|
||||||
|
public function title(): string { return 'Step 2 — Environment Check'; }
|
||||||
|
public function blurb(): string {
|
||||||
|
return 'Before touching updates, review the hosting environment.';
|
||||||
|
}
|
||||||
|
public function sub_items(): array {
|
||||||
|
return [
|
||||||
|
'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)',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
};
|
||||||
21
includes/steps/30-core.php
Normal file
21
includes/steps/30-core.php
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
if (!defined('ABSPATH')) exit;
|
||||||
|
|
||||||
|
return new class extends WPH_Step {
|
||||||
|
public function id(): string { return 'core'; }
|
||||||
|
public function title(): string { return 'Step 3 — WordPress Core Update'; }
|
||||||
|
public function sub_items(): array {
|
||||||
|
return [
|
||||||
|
'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',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
public function watch_outs(): array {
|
||||||
|
return [
|
||||||
|
'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.',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
};
|
||||||
25
includes/steps/40-plugins.php
Normal file
25
includes/steps/40-plugins.php
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
if (!defined('ABSPATH')) exit;
|
||||||
|
|
||||||
|
return new class extends WPH_Step {
|
||||||
|
public function id(): string { return 'plugins'; }
|
||||||
|
public function title(): string { return 'Step 4 — Plugin Updates'; }
|
||||||
|
public function sub_items(): array {
|
||||||
|
return [
|
||||||
|
'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',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
public function watch_outs(): array {
|
||||||
|
return [
|
||||||
|
'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',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
};
|
||||||
20
includes/steps/50-theme.php
Normal file
20
includes/steps/50-theme.php
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<?php
|
||||||
|
if (!defined('ABSPATH')) exit;
|
||||||
|
|
||||||
|
return new class extends WPH_Step {
|
||||||
|
public function id(): string { return 'theme'; }
|
||||||
|
public function title(): string { return 'Step 5 — Theme Updates'; }
|
||||||
|
public function sub_items(): array {
|
||||||
|
return [
|
||||||
|
'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',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
public function watch_outs(): array {
|
||||||
|
return [
|
||||||
|
'Layout changes post-theme update, broken header/footer, missing custom fonts or colours — indicates customisation was done directly in the parent theme.',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
};
|
||||||
23
includes/steps/60-visual.php
Normal file
23
includes/steps/60-visual.php
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
if (!defined('ABSPATH')) exit;
|
||||||
|
|
||||||
|
return new class extends WPH_Step {
|
||||||
|
public function id(): string { return 'visual'; }
|
||||||
|
public function title(): string { return 'Step 6 — Visual and Functional Check'; }
|
||||||
|
public function blurb(): string {
|
||||||
|
return 'Do a manual walkthrough of the site.';
|
||||||
|
}
|
||||||
|
public function sub_items(): array {
|
||||||
|
return [
|
||||||
|
'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',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
};
|
||||||
16
includes/steps/70-performance.php
Normal file
16
includes/steps/70-performance.php
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<?php
|
||||||
|
if (!defined('ABSPATH')) exit;
|
||||||
|
|
||||||
|
return new class extends WPH_Step {
|
||||||
|
public function id(): string { return 'performance'; }
|
||||||
|
public function title(): string { return 'Step 7 — Performance Check'; }
|
||||||
|
public function sub_items(): array {
|
||||||
|
return [
|
||||||
|
'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',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
};
|
||||||
17
includes/steps/80-security.php
Normal file
17
includes/steps/80-security.php
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<?php
|
||||||
|
if (!defined('ABSPATH')) exit;
|
||||||
|
|
||||||
|
return new class extends WPH_Step {
|
||||||
|
public function id(): string { return 'security'; }
|
||||||
|
public function title(): string { return 'Step 8 — Security Check'; }
|
||||||
|
public function sub_items(): array {
|
||||||
|
return [
|
||||||
|
'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',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
};
|
||||||
14
includes/steps/90-database.php
Normal file
14
includes/steps/90-database.php
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<?php
|
||||||
|
if (!defined('ABSPATH')) exit;
|
||||||
|
|
||||||
|
return new class extends WPH_Step {
|
||||||
|
public function id(): string { return 'database'; }
|
||||||
|
public function title(): string { return 'Step 9 — Database'; }
|
||||||
|
public function sub_items(): array {
|
||||||
|
return [
|
||||||
|
'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',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
};
|
||||||
32
site-healthcheck.php
Normal file
32
site-healthcheck.php
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Plugin Name: 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.0
|
||||||
|
* Author: Steve Hanlon
|
||||||
|
* License: GPL-2.0-or-later
|
||||||
|
* Requires PHP: 7.4
|
||||||
|
*
|
||||||
|
* Internal/agency tool — not distributed via WP.org. See beads decision hc-5ix.26.
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (!defined('ABSPATH')) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
define('WPH_VERSION', '0.1.0');
|
||||||
|
define('WPH_PLUGIN_FILE', __FILE__);
|
||||||
|
define('WPH_PLUGIN_DIR', plugin_dir_path(__FILE__));
|
||||||
|
define('WPH_PLUGIN_URL', plugin_dir_url(__FILE__));
|
||||||
|
define('WPH_OPT_SESSION', 'wph_session');
|
||||||
|
|
||||||
|
require_once WPH_PLUGIN_DIR . 'includes/class-wph-step.php';
|
||||||
|
require_once WPH_PLUGIN_DIR . 'includes/class-wph-steps.php';
|
||||||
|
require_once WPH_PLUGIN_DIR . 'includes/class-wph-session.php';
|
||||||
|
require_once WPH_PLUGIN_DIR . 'includes/recovery-bootstrap.php';
|
||||||
|
require_once WPH_PLUGIN_DIR . 'includes/admin-page.php';
|
||||||
|
require_once WPH_PLUGIN_DIR . 'includes/report.php';
|
||||||
|
|
||||||
|
add_action('plugins_loaded', function () {
|
||||||
|
WPH_Steps::instance()->discover(WPH_PLUGIN_DIR . 'includes/steps/');
|
||||||
|
});
|
||||||
168
steps.md
Normal file
168
steps.md
Normal file
@@ -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.*
|
||||||
Reference in New Issue
Block a user